레코드 클래스 공식문서 정리

이동영·2024년 3월 8일

자바 개념정리

목록 보기
4/21

레코드 클래스란 일반 클래스와 다르게 간단하게 데이터를 저장하고 사용할 수 있으며
헤더에 내용을 선언한다. 기본적으로 equals, hashCode, toString 메서드가 자동으로 생성되어 제공된다.

예를들어 이렇게 선언할 수 있다.

record Rectangle(double length, double width) { }```

간단한 이 문장은 다음 코드와 동일하다.

public final class Rectangle {
    private final double length;
    private final double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    double length() { return this.length; }
    double width()  { return this.width; }

    // Implementation of equals() and hashCode(), which specify
    // that two record objects are equal if they
    // are of the same type and contain equal field values.
    public boolean equals...
    public int hashCode...

    // An implementation of toString() that returns a string
    // representation of all the record class's fields,
    // including their names.
    public String toString() {...}
}

레코드 클래스 선언하기

  • 레코드는 이름, 매개변수, 본문으로 구성이 되어있다.
  • 레코드 클래스는 자동적으로 다음 멤버를 선언한다.

헤더에는 두개의 구성 요소가 있다!!

  1. private final 필드와 함께 같은 이름 그리고 레코드 컴포넌트로 선언한다.
  2. public 접근제어자 메서드와 함께 같은 타입의 이름과 컴포넌트 즉 예제의
    double length, double width이다.
  • 기본 생성자의 선언부와 동일하며 이 생성자는 각 인자로 부터 할당받은 레코드 클래스의 컴포넌트 필드로 인스턴스화 한다.
  • equals와 hashCode()를 구현하여 두개의 레코드 클래스 객체를 지정하여 동일한 컴포넌트 값을 포함하는 경우 동등하게 인식된다.
  • 모든 레코드 클래스 컴포넌트의 변수의 값을 표현하는 toString메서드를 구현한다.
  • 레코드 클래스는 좀 특별한 종류의 클래스이며 new키워드를 사용하여 레코드 객체를 생성할 수 있다.

레코드 클래스의 정식 생성자

다음 예제에서는 레코드 클래스의 정식 생성자를 명시적으로 선언한다.

  • 길이와 넓이가 0보다 큰지 확인한다. 조건을 만족하지 않을 경우IllegalArgumentException 예외를 발생시킨다.
record Rectangle(double length, double width) {
    public Rectangle(double length, double width) {
        if (length <= 0 || width <= 0) {
            throw new java.lang.IllegalArgumentException(
                String.format("Invalid dimensions: %f, %f", length, width));
        }
        this.length = length;
        this.width = width;
    }
}
  • 기본 생성자 선언부에서 컴포넌트를 반복하는것?은 번거롭고 오류가 발생할 수 있다.
  • 이를 방지하려면 명시적으로 생성자를 선언해줘야 한다.

예를들어 다음 예제의 기본 생성자 선언은 이전 예제와 동일한 방식으로 길이와 넓이의 유효성을 검사한다.

여기서 잠깐 Compact Constructor란?

record생성자가 private필드를 초기화 하는것보다 더 많은 행동을 하기 원할때는 생성자를 커스텀 할 수 있다. 이때 Class Constructor보다 더 간단하게 적을 수 있다.

Compact Constructor의 특징

  • 파라미터를 작성하지 않아도 된다.
  • 초기화 로직은 마지막에 호출된다?
public RecordCarsDto {  // 매개변수를 받는 부분이 생략됨
    if (Objects.isNull(values)) {
        values = new ArrayList<>();
    }
    if (Objects.isNull(speed)) {
        speed = 10;
    }
    // this.values = values; this.speed = speed; 와 같은 초기화 로직은 마지막에 자동으로 호출해줌.
}

출저 : https://velog.io/@leverest96/Compact-Constructor

record Rectangle(double length, double width) {
    public Rectangle {
        if (length <= 0 || width <= 0) {
            throw new java.lang.IllegalArgumentException(
                String.format("Invalid dimensions: %f, %f", length, width));
        }
    }
}
  • 컴팩트 생성자는 오직 레코드 클래스 안에서만 사용할 있다.
  • 표준 생성자에서 선언하는것은 콤펙트 생성자에서는 선언하지 않는다.
  • 컴팩트 생성자 끝에서 명시적으로 할당된 레코드 클래스의 private 필드는 컴포넌트에 해당된다.

명시적으로 레코드 클래스의 멤버들을 선언한다.

  • 헤더에서 파생된 멤버들을 명시적으로 선언할 수 있다.
  • 예를들어 레코드 클래스에 해당되는 public접근 제어자 메서드를 명시적으로 선언할 수 있다.
record Rectangle(double length, double width) {
 
    // Public accessor method
    public double length() {
        System.out.println("Length is " + length);
        return length;
    }
}
  • length()는 length필드값을 반환하고 출력한다.
  • 이 메서드는 length필드에 대한 접근자 역활을 한다.
  • 이렇게 하면 Rectangle레코드 객체의 length()를 사용하여 length필드 값을 알 수 있다.
  • 만약 자체적으로 접근자 메서드를 구현하면 암시적으로 구현된 접근자와 동일한 - 특성을 갖도록 구현해야 한다.
  • 예를들어 public으로 선언해야 하며 동일한 타입을 반환하도록 해야 한다.
  • 마찬가지로 hashCode와 equals를 구현한경우 java.lang.Record클래스와 동일한 특성과 동작을 갖도록 해야 한다.

레코드 클래스에서도 동일하게 static필드 static초기화 static메서드를 구현할 수 있다.
이들은 일반 클래스에서와 같이 동일하게 동작한다.

record Rectangle(double length, double width) {
    
    // Static field
    static double goldenRatio;

    // Static initializer
    static {
        goldenRatio = (1 + Math.sqrt(5)) / 2;
    }

    // Static method
    public static Rectangle createGoldenRectangle(double width) {
        return new Rectangle(width, width * goldenRatio);
    }
}
  • 레코드 클래스에서는 인스턴스 필드 인스턴스 생성자를 선언할 수 없다.
  • 예를들어 다음과 같은 레코드 클래스 선언은 컴파일이 되지 않는다.
record Rectangle(double length, double width) {

    // Field declarations must be static:
    BiFunction<Double, Double, Double> diagonal;

    // Instance initializers are not allowed in records:
    {
        diagonal = (x, y) -> Math.sqrt(x*x + y*y);
    }
}
  • 레코드 클래스에서는 자체적인 접근제어자 메서드 구현하든 말든 상관없이 인스턴스 메서드를 선언할 수 있다. 중첩클래스, 인터페이스, 중첩레코드를(암묵적으로 정적인)선언할 수 있다. 예를들어서
record Rectangle(double length, double width) {

    // Nested record class
    record RotationAngle(double angle) {
        public RotationAngle {
            angle = Math.toRadians(angle);
        }
    }
    
    // Public instance method
    public Rectangle getRotatedRectangleBoundingBox(double angle) {
        RotationAngle ra = new RotationAngle(angle);
        double x = Math.abs(length * Math.cos(ra.angle())) +
                   Math.abs(width * Math.sin(ra.angle()));
        double y = Math.abs(length * Math.sin(ra.angle())) +
                   Math.abs(width * Math.cos(ra.angle()));
        return new Rectangle(x, y);
    }
}

레코드 클래스에서는 네이티브 메서드를 선언할 수 없다.

레코드 클래스의 특징

  • 레코드 클래스는 묵시적으로 final키워드가 붙었으며 클래스를 명시적으로 확장할 수 없다.
  • 하지만 이 이외에는 일반 클래스와 동일하게 동작한다.
record Triangle<C extends Coordinate> (C top, C left, C right) { }

레코드 클래스에서 하나의 인터페이스를 구현할 수 있다.

record Customer(...) implements Billable { }

레코드 클래스 및 컴포넌트에 어노테이션을 추가할 수 있다.


import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface GreaterThanZero { }
record Rectangle(
    @GreaterThanZero double length,
    @GreaterThanZero double width) { }
  • 레코드 클래스의 컴포넌트를 어노테이션으로 처리하게 되면 해당 어노테이션이 멤버 및 생성자에도 적용될 수 있다.
  • 해당 어노테이션을 한곳에 붙이게 되면 레코드 클래스의 필드 메서드등 다른 부분에도 영향을 미칠 수 있게 된다.
  • 어노테이션이 전파되는것은 어노테이션이 어떤 범위에 적용 가능한지에 따라 결정된다.
public final class Rectangle {
    private final @GreaterThanZero double length;
    private final @GreaterThanZero double width;
    
    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    
    double length() { return this.length; }
    double width() { return this.width; }
}

레코드 클래스는 sealed classes(밀봉 클래스)

sealed classes는 간단하게 extends, impliments할 클래스를 지정하고 상속 혹은 구현을 허용하는 키워드 이다.

public sealed interface CarBrand permits Hyundai, Kia{}

public final class Hyundai implements CarBrand {}
public non-sealed class Kia implements CarBrand {}

레코드 클래스는 sealed classes와 잘 동작한다.

지역 레코드 클래스

  • 지역 레코드 클래스는 지역 클래스와 유사하다.
  • 다음 예제에서는 상인을 Merchant 레코드 클래스로 모델링 하고 상인에 의해 - 판매를 Sale레코드 클래스로 모델링 한다.
  • Merchant와 Sale은 모두 최상위 레코드 클래스 이다.
  • 상인과 해당 상인의 매출을 총 집계하는것은 findTopMerchants메서드 내 - - 선언된 지역 레코드 클래스로 모델링 된다.
  • 이 지역 레코드 클래스는 스트림 연산의 가독성? 을 향상 시킨다.
import java.time.*;
import java.util.*;
import java.util.stream.*;

record Merchant(String name) { }

record Sale(Merchant merchant, LocalDate date, double value) { }

public class MerchantExample {
    
    List<Merchant> findTopMerchants(
        List<Sale> sales, List<Merchant> merchants, int year, Month month) {
    
        // Local record class
        record MerchantSales(Merchant merchant, double sales) {}

        return merchants.stream()
            .map(merchant -> new MerchantSales(
                merchant, this.computeSales(sales, merchant, year, month)))
            .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
            .map(MerchantSales::merchant)
            .collect(Collectors.toList());
    }   
    
    double computeSales(List<Sale> sales, Merchant mt, int yr, Month mo) {
        return sales.stream()
            .filter(s -> s.merchant().name().equals(mt.name()) &&
                s.date().getYear() == yr &&
                s.date().getMonth() == mo)
            .mapToDouble(s -> s.value())
            .sum();
    }    

    public static void main(String[] args) {
        
        Merchant sneha = new Merchant("Sneha");
        Merchant raj = new Merchant("Raj");
        Merchant florence = new Merchant("Florence");
        Merchant leo = new Merchant("Leo");
        
        List<Merchant> merchantList = List.of(sneha, raj, florence, leo);
        
        List<Sale> salesList = List.of(
            new Sale(sneha,    LocalDate.of(2020, Month.NOVEMBER, 13), 11034.20),
            new Sale(raj,      LocalDate.of(2020, Month.NOVEMBER, 20),  8234.23),
            new Sale(florence, LocalDate.of(2020, Month.NOVEMBER, 19), 10003.67),
            // ...
            new Sale(leo,      LocalDate.of(2020, Month.NOVEMBER,  4),  9645.34));
        
        MerchantExample app = new MerchantExample();
        
        List<Merchant> topMerchants =
            app.findTopMerchants(salesList, merchantList, 2020, Month.NOVEMBER);
        System.out.println("Top merchants: ");
        topMerchants.stream().forEach(m -> System.out.println(m.name()));
    }
}
  • 중첩된 레코드 클래스와 마찬가지로 지역 레코드 클래스도 묵시적으로 static이므로 해당 클래스의 메서드는 외부 메서드의 변수에 접근할 수 없다.
  • 이는 지역 클래스와 다르게 지역 레코드 클래스는 항상 static이다.

static 맴버의 Inner Class

Java SE 16 이전에는 내부 클래스에서 맴버가 상수인 경우에만 명시적 혹은 묵시적으로 static 멤버를 선언할 수 있었다.

  • 즉 중첩된 레코드 클래스가 묵시적으로 static이기에 내부 클래스에서 레코드 클래스 멤버를 선언할 수 없다.
  • 하지만 자바 SE 16부터는 내부 클래스에서 명시적 또는 묵시적으로 정적멤버를 선언할 수 있게 되었다.
  • 이것은 레코드 클래스 멤버를 포함한다.
public class ContactList {
    
    record Contact(String name, String number) { }
    
    public static void main(String[] args) {
        
        class Task implements Runnable {
            
            // Record class member, implicitly static,
            // declared in an inner class
            Contact c;
            
            public Task(Contact contact) {
                c = contact;
            }
            public void run() {
                System.out.println(c.name + ", " + c.number);
            }
        }        
        
        List<Contact> contacts = List.of(
            new Contact("Sneha", "555-1234"),
            new Contact("Raj", "555-2345"));
        contacts.stream()
                .forEach(cont -> new Thread(new Task(cont)).start());
    }
}

레코드의 직렬화

  • 레코드 클래스의 인스턴스를 직렬화 혹은 역직렬화 를 할 수 있지만 writeObject, readObject, readObjectNoData, writeExternal 또는 readExternal 메서드를 제공하여 프로세스를 사용자 정의 할 수 없다.레코드 클래스의 구성 요소는 직렬화를 지배하고, 레코드 클래스의 정규 생성자는 역직렬화를 지배합니다. 더 많은 정보와 확장된 예제는 "Serializable Records"를 참조하십시오. 또한 Java Object Serialization Specification의 "Serialization of Records"

레코드 클래스 관련 API

  • java.lang.Record는 추상클래스는 모든 레코드 클래스의 공통 상위 클래스 이다.
  • 소스 파일이 java.lang 이외 패키지에서 Record 클래스를 가져오게 되면 컴파일 오류가 발생할 수 있다.
  • 자바 소스 파일은 묵시적으로 import java.lang.* 을 통해 java.lang의 모든 패키지 유형을 가져온다.
  • java.lang.Record 클래스를 포함한다.

com.myapp.Record선언을 고려해보자

package com.myapp;

public class Record {
    public String greeting;
    public Record(String greeting) {
        this.greeting = greeting;
    }
}

다음 예제인 org.example.MyappPackageExample은 와일드카드를 사용하여 com.myapp.Record를 가져오지만 컴파일되지 않습니다.


package org.example;
import com.myapp.*;

public class MyappPackageExample {
    public static void main(String[] args) {
       Record r = new Record("Hello world!");
    }
}

컴파일러는 다음과 유사한 오류 메시지를 생성합니다:

./org/example/MyappPackageExample.java:6: error: reference to Record is ambiguous
       Record r = new Record("Hello world!");
       ^
  both class com.myapp.Record in com.myapp and class java.lang.Record in java.lang match

./org/example/MyappPackageExample.java:6: error: reference to Record is ambiguous
       Record r = new Record("Hello world!");
                      ^
  both class com.myapp.Record in com.myapp and class java.lang.Record in java.lang match

com.myapp 패키지의 Record와 java.lang 패키지의 Record가 모두 와일드카드로 가져 왔습니다. 결과적으로 두 클래스 모두 우선되지 않으므로 컴파일러는 간단한 이름 Record 사용 시 오류를 생성합니다.

이 예제를 컴파일할 수 있도록 하려면 import 문을 변경하여 Record의 완전한 패키지 경로를 가져오도록 해야합니다.

import com.myapp.Record;

참고: java.lang 패키지에 클래스를 도입하는 것은 드물지만 Enum(Java SE 5), Module(Java SE 9) 및 Record(Java SE 14)와 같이 때때로 필요합니다.

java.lang.Class 클래스에는 레코드 클래스와 관련된 두 가지 메서드가 있다.

  1. RecordComponent[] getRecordComponents(): 레코드 클래스의 구성 요소에 해당하는 java.lang.reflect.RecordComponent 객체의 배열을 반환합니다.
  2. boolean isRecord(): isEnum()과 유사하지만 클래스가 레코드 클래스로 선언되었으면 true를 반환한다.
profile
가치를 제공하는 개발자

0개의 댓글