영권's

Java16 - record 본문

자바/JAVA

Java16 - record

ykkkk 2021. 9. 19. 06:51

record 

Java 16에서부터는 record라는 형태의 클래스를 사용할 수 있다.

 

record 선언의 헤더에 내용에 필요한 값을 지정합니다.

그러면 적절한 접근자, constructor, equals, hashCode, and toString 메서드가 자동으로 생성됩니다.

 

레코드의 필드는 클래스가 단순한 "데이터 운반" 역할을 하기 때문에 final 필드입니다.

 

예를 들어 다음과 같이 생성한 클래스와 record는 동일합니다.

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() {...}
}

 

레코드 클래스 선언은 이름, 매개변수(Optional), 레코드의 구성요소들을 나열하는 헤더 및 본문으로 구성됩니다.

 

그래서 레코드를 생성하면 레코드에 선언된 다음 멤버를 자동으로 선언합니다.

  • 헤더의 각 구성 요소에 대해 다음 두 멤버:
    • 레코드의 구성요소와 이름이 같고 선언된 타입을 가진 private final 필드입니다.
      이 필드를 Component 필드라고도 합니다.
    • 구성요소들과 이름과 유형이 동일한 공용 접근자 메서드입니다.
      • 직사각형 레코드 클래스 예제에서 이러한 메서드는 Rectangle::length() and Rectangle::width() 입니다.
    • 시그니처가 헤더와 동일한 생성자입니다. 이 생성자는 레코드 클래스를 인스턴스화하는 새 식에서 해당 Component 필드에 각 인수를 할당합니다. (헤더부분에 매개변수로 할당한 필드에 대한 생성자를 만든다. 기본생성자 x)
    • equals 및 hashcode 메서드의 구현. 두 레코드 클래스가 동일한 유형이고 구성 요소 값이 동일한 경우 동일하다고 지정합니다.
    • 모든 레코드 클래스 구성 요소의 문자열 표현을 이름과 함께 포함하는 toString 메서드의 구현입니다.

 

레코드 클래스는 클래스의 하나의 특별한 종류일 뿐이므로 new키워드 를 사용하여 레코드 개체(레코드 클래스의 인스턴스)를 만듭니다. 

Rectangle r = new Rectangle(4,5);

 

The Canonical Constructor of a Record Class

다음 예에서는 직사각형 레코드 클래스에 대한 표준 생성자를 명시적으로 선언합니다.

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

 

하지만 매번 표준 생성자의 시그니처에 레코드 클래스의 구성 요소를 반복하는 것은 번거롭고 오류가 발생하기 쉽습니다.

이를 방지하기 위해 시그니처가 암시적(Component 에서 자동으로 파생됨)인 "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));
        }
    }
}

이런 간결한 형태의 생성자 선언은 레코드 클래스에서만 사용할 수 있습니다.

그리고 this.length = length; 그리고 this.width = width; 표준 생성자에 있던것이 compact constructor에 나타나지 않습니다.

compact constructor의 끝에서 암묵적 record 선언 시 작성한 매개변수에 해당하는 구성 요소에 해당하는 레코드 클래스의 개인 필드에 자동으로 할당합니다.

 

Explicit Declaration of Record Class Members(레코드 클래스 멤버의 명시적 선언)

예를 들어 레코드 클래스의 헤더에서 파생된 멤버인 Component에 접근하기 위한 public 메서드를 명시적으로 선언할 수 있습니다.

record Rectangle(double length, double width) {
 
    // Public accessor method
    public double length() {
        System.out.println("Length is " + length);
        return length;
    }
}

 

만약 어떤 이미 만들어진 고유한 메서드를 구현하는 경우, 이런 접근자가 암묵적으로 파생된 접근자와 동일한 특성을 가져야 합니다.

 

마찬가지로, equal, hashCode 및 toString 메서드의 사용자 구현하는 경우 해당 메서드의 특성과 동작이 java.lang과 동일해야 합니다.

모든 레코드 클래스의 공통 슈퍼 클래스인 레코드(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);
    }
}

 

레코드 클래스에서 인스턴스 변수(non-static fields) 또는 인스턴스 이니셜라이저를 선언할 수 없습니다.

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

 

사용자의 고유한 메서드를 구현하는지 여부와 상관없이 레코드 클래스에서 인스턴스 메서드를 선언할 수 있습니다. 

또한 중첩된 레코드 클래스(암묵정으로 static)를 포함하여 레코드 클래스에서 중첩된 클래스 및 인터페이스를 선언할 수 있습니다.  For example:

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

레코드 구성요소에 주석을 달면 주석이 레코드 클래스의 멤버 및 생성자에게 전파될 수 있습니다.

이 전파는 애너테이션이 적용되는 컨텍스트에 따라 결정됩니다.

이전 예제에서는 @Target(ElementType)입니다.(필드) @GreaterThanZero가 레코드 구성요소에 해당하는 필드에 전파되는 것을 말한다.

Record Classes and Sealed Classes and Interfaces

레코드 클래스는 sealed 클래스 및 인터페이스에서 잘 작동합니다.

 

Local Record Classes

로컬 레코드 클래스는 로컬 클래스와 유사하며 메서드 내부에 정의된 레코드 클래스입니다.

 

예시)

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

중첩된 레코드 클래스와 마찬가지로 로컬 레코드 클래스는 암묵적으로 정적입니다.

즉, 로컬 클래스와 달리 자체 메서드가 에워싸인 메서드의 변수에 액세스할 수 없습니다.

 

 

Inner record 선언
바이트 코드 확인 시 InnerClass가 final static으로 생성되는 것을 확인할 수 있다.

 

내부 클래스의 정적 멤버

Java SE 16 이전에는 내부 클래스에 명시적으로 또는 암묵적으로 정적 멤버를 선언할 수 없습니다.

해당 멤버가 상수 변수가 아니면 해당 멤버를 선언할 수 없습니다.

즉, 중첩된 레코드 클래스는 암묵적으로 static이기 때문에 내부 클래스가 레코드 클래스 멤버를 선언할 수 없습니다.

 

Java SE 16 이상에서 내부 클래스는 레코드 클래스 멤버를 포함하는 명시적으로 또는 암묵적으로 static 멤버를 선언할 수 있습니다. 다음 예제가 이를 보여줍니다.

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

 

레코드 클래스 관련 API

추상클래스 java.lang.Record은 모든 레코드 클래스의 공통 조상입니다.

 

소스 파일이 java.lang이 아닌 패키지에서 레코드라는 클래스를 가져오는 경우 컴파일러 오류가 발생할 수 있습니다.

예시)

package com.myapp;

public class Record {
    public String greeting;
    public Record(String greeting) {
        this.greeting = greeting;
    }
}
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

이 예제를 컴파일하려면 다음의 import 정규화된 이름을 가져오도록 명령문을 변경 해야 합니다.

import com.myapp.Record;

 


Record 참고
https://docs.oracle.com/en/java/javase/16/language/records.html

 

sealed 클래스 참고

https://openjdk.java.net/jeps/360

 

https://www.infoq.com/articles/java-sealed-classes/

 

Comments