Friday, April 10, 2020

Java 14'de Record Kullanımı

Java 14'de gelen yeniliklerden biri Record yapısıdır. Record özünde bir sınıf tanımlamaktadır. Dolayısı ile record tipinden bir nesne yaratabilirsiniz. Ama bu nesnenin durumu değiştirilemez! Bu Java'da yabancısı olmadığımız bir durum. String sınıfı, basit tiplere karşı düşen sınıflarımız (örneğin Integer, Character, Double gibi), BigInteger, BigDecimal sınıflarının hepsi bu türden durumu değiştirilemez (=immutable) nesneler yaratabildiğimiz sınıflardır.

IntelliJ IDEA 2020.1'de yeni bir programlama elemanı yaratmak istediğimizde listede artık Record'da ön izleme kullanımında bir özellik olarak yerini alıyor:

Java 14'de yeni gelen Ön İzlemede bir özellik olarak Record.

package com.example;

import java.awt.*;

public record Point3D(double x, double y, double z,
                      Color color) implements Comparable<Point3D>, Translateable<Point3D> {
    public static final Point3D ORIGIN = Point3D.of(0, 0, 0, Color.BLACK);

    public static Point3D of(double x, double y, double z, Color color) {
        return new Point3D(x, y, z, color);
    }

    public static double l2Distance(Point3D p1, Point3D p2) {
        return Math.sqrt(Math.pow(p1.x - p2.y, 2.) +
                Math.pow(p1.y - p2.y, 2.) +
                Math.pow(p1.z - p2.z, 2.));
    }

    public double l2Distance(Point3D other) {
        return l2Distance(this, other);
    }

    public double l2Distance() {
        return l2Distance(this, ORIGIN);
    }

    @Override
    public int compareTo(Point3D other) {
        return Double.compare(this.l2Distance(), other.l2Distance());
    }

    @Override
    public Point3D move(Point3D t) {
        return Point3D.of(this.x + t.x, this.y + t.y, this.z + t.z, this.color);
    }
}

package com.example;

public interface Translateable<T> {
    T move(T t);
}

Yukarıda Point3D isimli bir record tanımlanmış olduk. record, final sınıf özelliğindedir. Dolayısı ile record kullanarak yeni bir sınıf türetemezsiniz. record tanıtımı bir kurucu fonksiyon tanımı içerir. Bu kurucu fonksiyonun parametreleri aynı zamanda öznitelikleri tanımlar ve ilklendirir. Bu öz nitelikler de final tanımlıdır. Onları da nesne yaratıldıktan sonra değerlerini değiştiremezsiniz.  Ayrıca record içinde kurucuda verilen parametrelerin belirlediği öz nitelikler dışında başka bir öz nitelik tanımlayamazsınız! Bu üyeler final bile olsa bunu yapamazsınız! Öz niteliklerin değerini dönecek metotlar otomatik olarak üretiliyor. Ancak bu metotların isimlendirilmesinde Java Beans isimlendirme kuralından farklı olarak doğrudan öz niteliğin adı kullanılır: x(), y(), z() ve color(). Ayrıca Object sınıfından gelen equals, hashCode ve toString metotlarına  özel olarak bu yapı için özel işlev yüklenmiştir. Bu işlevleri aşağıdaki örnek uygulama üzerinden görmeye ve anlamaya çalışalım:

package com.example;

import java.awt.*;
import java.util.List;

public class StudyRecord {
    public static void main(String[] args) {
        var p1 = new Point3D(1, 2, 3, Color.BLUE);
        var p2 = new Point3D(1, 2, 3, Color.GREEN);
        var p3 = new Point3D(1, 2, 3, Color.BLUE);
        var points = List.of(p1, p2, p3);
        points.forEach(p -> {
            System.out.println(p.x());
            System.out.println(p.y());
            System.out.println(p.z());
            System.out.println(p.color());
            System.out.println(p.toString());
            System.out.println(p.hashCode());
        });
        System.out.println(p1.equals(p2));
        System.out.println(p1.equals(p3));
        System.out.println(p2.equals(p3));
    }
}

Yukarıdaki uygulama çalıştırıldığında aşağıdaki çıktıyı üretecektir:

1.0
2.0
3.0
java.awt.Color[r=0,g=0,b=255]
Point3D[x=1.0, y=2.0, z=3.0, color=java.awt.Color[r=0,g=0,b=255]]
2047344895
1.0
2.0
3.0
java.awt.Color[r=0,g=255,b=0]
Point3D[x=1.0, y=2.0, z=3.0, color=java.awt.Color[r=0,g=255,b=0]]
2047409920
1.0
2.0
3.0
java.awt.Color[r=0,g=0,b=255]
Point3D[x=1.0, y=2.0, z=3.0, color=java.awt.Color[r=0,g=0,b=255]]
2047344895
false
true
false

recordDDD'deki Value Object'i gerçeklemek için kullanılabilir. Nesnenin kimliği yoktur ya da nesnenin kimliği tüm öz niteliklerinden oluşur. İki Point3D nesnesinin eşitliği için tüm üyelerinin eşit olmasına bakılır. 

record içinde static üye tanıtabilirsiniz. Yukarıdaki Point3D örneğinde olduğu gibi record bir ya da daha fazla arayüzü (örneğimizde Comparable ve Translateable) gerçekleyebilir. 

record içinde bir sınıf içinde yazabildiğiniz türden sıradan metotlar yazabilirsiniz. Ancak bu metotlar nesnenin durumunuzu doğal olarak değiştiremeyecektir.

Şimdi JDK içinden çıkan javap komutunu kullanarak Point3D sınıfının tüm üyelerine bir göz atalım:

javap -classpath . com.example.Point3D
Compiled from "Point3D.java"
public final class com.example.Point3D extends java.lang.Record implements java.lang.Comparable<com.example.Point3D>, com.example.Translateable<com.example.Point3D> {
  public static final com.example.Point3D ORIGIN;
  public com.example.Point3D(double, double, double, java.awt.Color);
  public static com.example.Point3D of(double, double, double, java.awt.Color);
  public static double l2Distance(com.example.Point3D, com.example.Point3D);
  public double l2Distance(com.example.Point3D);
  public double l2Distance();
  public int compareTo(com.example.Point3D);
  public com.example.Point3D move(com.example.Point3D);
  public java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public double x();
  public double y();
  public double z();
  public java.awt.Color color();
  public int compareTo(java.lang.Object);
  static {};
}

Komutun ürettiği çıktıdan Point3D sınıfının, Java 14 ile gelen java.lang.Record soyut sınıfından türetildiği anlaşılıyor:

package java.lang;

@jdk.internal.PreviewFeature(feature=jdk.internal.PreviewFeature.Feature.RECORDS, essentialAPI=true)
public abstract class Record {
    /**
     * Constructor for record classes to call.
     */
    protected Record() {}

    /**
     * Indicates whether some other object is "equal to" this one.  In addition
     * to the general contract of {@link Object#equals(Object) Object.equals},
     * record classes must further obey the invariant that when
     * a record instance is "copied" by passing the result of the record component
     * accessor methods to the canonical constructor, as follows:
     * <pre>
     *     R copy = new R(r.c1(), r.c2(), ..., r.cn());
     * </pre>
     * then it must be the case that {@code r.equals(copy)}.
     *
     * @implSpec
     * The implicitly provided implementation returns {@code true} if
     * and only if the argument is an instance of the same record type
     * as this object, and each component of this record is equal to
     * the corresponding component of the argument; otherwise, {@code
     * false} is returned. Equality of a component {@code c} is
     * determined as follows:
     * <ul>
     *
     * <li> If the component is of a reference type, the component is
     * considered equal if and only if {@link
     * java.util.Objects#equals(Object,Object)
     * Objects.equals(this.c(), r.c()} would return {@code true}.
     *
     * <li> If the component is of a primitive type, using the
     * corresponding primitive wrapper class {@code PW} (the
     * corresponding wrapper class for {@code int} is {@code
     * java.lang.Integer}, and so on), the component is considered
     * equal if and only if {@code
     * PW.valueOf(this.c()).equals(PW.valueOf(r.c()))} would return
     * {@code true}.
     *
     * </ul>
     *
     * The implicitly provided implementation conforms to the
     * semantics described above; the implementation may or may not
     * accomplish this by using calls to the particular methods
     * listed.
     *
     * @see java.util.Objects#equals(Object,Object)
     *
     * @param   obj   the reference object with which to compare.
     * @return  {@code true} if this object is equal to the
     *          argument; {@code false} otherwise.
     */
    @Override
    public abstract boolean equals(Object obj);

    /**
     * Obeys the general contract of {@link Object#hashCode Object.hashCode}.
     *
     * @implSpec
     * The implicitly provided implementation returns a hash code value derived
     * by combining the hash code value for all the components, according to
     * {@link Object#hashCode()} for components whose types are reference types,
     * or the primitive wrapper hash code for components whose types are primitive
     * types.
     *
     * @see     Object#hashCode()
     *
     * @return  a hash code value for this object.
     */
    @Override
    public abstract int hashCode();

    /**
     * Obeys the general contract of {@link Object#toString Object.toString}.
     *
     * @implSpec
     * The implicitly provided implementation returns a string that is derived
     * from the name of the record class and the names and string representations
     * of all the components, according to {@link Object#toString()} for components
     * whose types are reference types, and the primitive wrapper {@code toString}
     * method for components whose types are primitive types.
     *
     * @see     Object#toString()
     *
     * @return  a string representation of the object.
     */
    @Override
    public abstract String toString();
}

Dilerseniz record sınıfına istediğiniz kadar kurucu fonksiyon yazabilirsiniz:

public record Point3D(double x, double y, double z, Color color) implements Comparable<Point3D>, Translateable<Point3D> {
    public Point3D(double x, double y, double z, Color color) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.color = color;
    }

    public Point3D(double x, double y, double z) {
        this(0, 0, z, Color.BLACK);
    }

    public Point3D(double x, double y) {
        this(0, 0, 1, Color.BLACK);
    }

    public Point3D() {
        this(0, 0, 0);
    }
   
   .
   .
   .

}

Point3D(double x, double y, double z, Color color) kurucusunu eğer parametreleri ilgili üyelere atamak dışında bir denetim yapmayacaksanız yazmanıza gerek bulunmuyor:

public record Point3D(double x, double y, double z, Color color) implements Comparable<Point3D>, Translateable<Point3D> {

    public Point3D(double x, double y, double z) {
        this(0, 0, z, Color.BLACK);
    }

    public Point3D(double x, double y) {
        this(0, 0, 1, Color.BLACK);
    }

    public Point3D() {
        this(0, 0, 0);
    }
   
   .
   .
   .

}

Standart kurucu fonksiyonu, değişmezleri doğrulamak için kod eklemeniz gerektiğinde yeniden tanımlayın:

public record Point3D(double x, double y, double z, Color color) implements Comparable<Point3D>, Translateable<Point3D> {

    public Point3D(double x, double y, double z, Color color) {
        if(Double.isFinite(x) && Double.isFinite(y) && Double.isFinite(z)){
            this.x = x;
            this.y = y;
            this.z = z;
            this.color = color;
            return;        
        }
        throw new IllegalArgumentException("Point3D should be finite!");
    }

    public Point3D(double x, double y, double z) {
        this(0, 0, z, Color.BLACK);
    }

    public Point3D(double x, double y) {
        this(0, 0, 1, Color.BLACK);
    }

    public Point3D() {
        this(0, 0, 0);
    }
   
   .
   .
   .

}

Java 14'de Record tipini nerede kullanabileceğimize bir bakalım:

var p1 = new Point3D(1, 2, 3, Color.BLUE);
var p2 = new Point3D(1, 2, 3, Color.GREEN);
var p3 = new Point3D(1, 2, 3, Color.BLUE);
var c1 = new Customer("1","Jack Bauer","jack.bauer@example.com","555-555-5555");
var c2 = new Customer("2","Kate Austen","kate.austen@example.com","555-555-5123");
var c3 = new Customer("3","James Sawyer","james.sawyer@example.com","555-555-5432");
List<Record> records = List.of(p1, p2, p3,c1,c2,c3);
records.forEach(rec -> {
    System.out.println(rec.toString());
    System.out.println(rec.hashCode());
});

1 comment: