Pengenalan Nilai Auto

1. Gambaran keseluruhan

AutoValue adalah penjana kod sumber untuk Java, dan lebih khusus lagi ia adalah pustaka untuk menghasilkan kod sumber untuk objek nilai atau objek yang diberi nilai .

Untuk menghasilkan objek jenis-nilai yang perlu anda lakukan ialah memberi penjelasan pada kelas abstrak dengan anotasi @AutoValue dan menyusun kelas anda. Apa yang dihasilkan adalah objek nilai dengan kaedah aksesor , konstruktor parameter, diganti dengan betul ke kaedah String (), sama dengan (Objek) dan hashCode () .

Coretan kod berikut adalah contoh ringkas kelas abstrak yang apabila disusun akan menghasilkan objek nilai bernama AutoValue_Person .

@AutoValue abstract class Person { static Person create(String name, int age) { return new AutoValue_Person(name, age); } abstract String name(); abstract int age(); } 

Mari kita teruskan dan mengetahui lebih lanjut mengenai objek nilai, mengapa kita memerlukannya dan bagaimana AutoValue dapat membantu menjadikan tugas menghasilkan dan memfaktur semula kod lebih sedikit memakan masa.

2. Persediaan Maven

Untuk menggunakan AutoValue dalam projek Maven, anda perlu memasukkan kebergantungan berikut dalam pom.xml :

 com.google.auto.value auto-value 1.2 

Versi terbaru boleh didapati dengan mengikuti pautan ini.

3. Objek Berbentuk Nilai

Jenis nilai adalah produk akhir perpustakaan, jadi untuk menghargai tempatnya dalam tugas pembangunan kita, kita mesti memahami jenis nilai, apa itu, apa yang sebenarnya dan mengapa kita memerlukannya.

3.1. Apakah Jenis Nilai?

Objek jenis nilai adalah objek yang kesamaan antara satu sama lain tidak ditentukan oleh identiti melainkan keadaan dalamannya. Ini bermaksud bahawa dua kejadian objek yang ditaip nilai dianggap sama asalkan mempunyai nilai medan yang sama.

Biasanya, jenis nilai tidak berubah . Bidang mereka mesti dibuat akhir dan mereka tidak harus mempunyai kaedah setter kerana ini akan menjadikannya berubah setelah instansiasi.

Mereka mesti menggunakan semua nilai medan melalui kaedah konstruktor atau kilang.

Jenis-nilai bukan JavaBeans kerana mereka tidak mempunyai konstruktor argumen lalai atau sifar dan mereka juga tidak mempunyai kaedah setter, demikian juga, mereka bukan Objek Pemindahan Data atau Objek Java Lama Plain .

Selain itu, kelas yang ditaip nilai mestilah muktamad, supaya tidak dapat diperpanjang, sekurang-kurangnya seseorang mengganti kaedahnya. JavaBeans, DTO dan POJO tidak semestinya muktamad.

3.2. Membuat Jenis Nilai

Dengan andaian kita ingin membuat jenis nilai yang disebut Foo dengan medan yang disebut teks dan nombor. Bagaimana kita akan melakukannya?

Kami akan menjadikan kelas akhir dan menandakan semua bidangnya sebagai final. Kemudian kita akan menggunakan IDE untuk menghasilkan konstruktor, kaedah hashCode () , kaedah sama (Objek) , pengambil sebagai kaedah wajib dan kaedah toString () , dan kita akan mempunyai kelas seperti:

public final class Foo { private final String text; private final int number; public Foo(String text, int number) { this.text = text; this.number = number; } // standard getters @Override public int hashCode() { return Objects.hash(text, number); } @Override public String toString() { return "Foo [text=" + text + ", number=" + number + "]"; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Foo other = (Foo) obj; if (number != other.number) return false; if (text == null) { if (other.text != null) return false; } else if (!text.equals(other.text)) { return false; } return true; } }

Setelah membuat contoh Foo , kami menjangkakan keadaan dalamannya akan tetap sama sepanjang kitar hidupnya.

Seperti yang akan kita lihat dalam subseksyen berikut , hashCode objek harus berubah dari contoh ke contoh , tetapi untuk jenis nilai, kita harus mengikatnya ke bidang yang menentukan keadaan dalaman objek nilai.

Oleh itu, walaupun menukar medan objek yang sama akan mengubah nilai hashCode .

3.3. Bagaimana Jenis-Jenis Berfungsi

Sebab-sebab jenis-nilai mesti tidak berubah adalah untuk mengelakkan sebarang perubahan pada keadaan dalamannya oleh aplikasi setelah dibuat.

Setiap kali kita ingin membandingkan dua objek yang ditaip nilai, kita mesti menggunakan kaedah sama (Objek) dari kelas Objek .

Ini bermaksud bahawa kita mesti selalu mengatasi kaedah ini dalam jenis nilai kita sendiri dan hanya akan kembali benar jika bidang objek nilai yang kita bandingkan mempunyai nilai yang sama.

Selain itu, bagi kita untuk menggunakan objek nilai kami dalam koleksi berdasarkan hash seperti HashSet s dan HashMap s tanpa berbuka, kita mesti betul melaksanakan Kodcincang () kaedah .

3.4. Mengapa Kita Memerlukan Nilai-Jenis

Keperluan untuk jenis nilai muncul dengan kerap. Ini adalah kes di mana kami ingin mengatasi tingkah laku lalai dari kelas Objek asal .

Seperti yang telah kita ketahui, pelaksanaan default dari objek Objek menganggap dua objek sama ketika mereka memiliki identiti yang sama namun untuk tujuan kita, kita menganggap dua objek sama ketika mereka mempunyai keadaan dalaman yang sama .

Dengan andaian kami ingin membuat objek wang seperti berikut:

public class MutableMoney { private long amount; private String currency; public MutableMoney(long amount, String currency) { this.amount = amount; this.currency = currency; } // standard getters and setters }

Kita boleh menjalankan ujian berikut untuk menguji persamaannya:

@Test public void givenTwoSameValueMoneyObjects_whenEqualityTestFails_thenCorrect() { MutableMoney m1 = new MutableMoney(10000, "USD"); MutableMoney m2 = new MutableMoney(10000, "USD"); assertFalse(m1.equals(m2)); }

Perhatikan semantik ujian.

Kami menganggapnya telah berlalu apabila kedua-dua objek wang itu tidak sama. Ini kerana kita belum mengatasi kaedah sama sehingga persamaan diukur dengan membandingkan rujukan memori objek, yang tentu saja tidak akan berbeza kerana objek tersebut berlainan yang menempati lokasi memori yang berbeza.

Setiap objek mewakili 10,000 USD tetapi Java memberitahu bahawa objek wang kita tidak sama . Kami mahu kedua-dua objek itu menguji tidak sama hanya apabila jumlah mata wang berbeza atau jenis mata wang berbeza.

Sekarang mari kita buat objek bernilai setara dan kali ini kita akan membiarkan IDE menghasilkan sebahagian besar kod:

public final class ImmutableMoney { private final long amount; private final String currency; public ImmutableMoney(long amount, String currency) { this.amount = amount; this.currency = currency; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (int) (amount ^ (amount >>> 32)); result = prime * result + ((currency == null) ? 0 : currency.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ImmutableMoney other = (ImmutableMoney) obj; if (amount != other.amount) return false; if (currency == null) { if (other.currency != null) return false; } else if (!currency.equals(other.currency)) return false; return true; } }

Satu-satunya perbezaan adalah kita mengatasi kaedah sama (Objek) dan hashCode () , sekarang kita mempunyai kawalan terhadap bagaimana kita mahu Java membandingkan objek wang kita. Mari jalankan ujian setaraf dengannya:

@Test public void givenTwoSameValueMoneyValueObjects_whenEqualityTestPasses_thenCorrect() { ImmutableMoney m1 = new ImmutableMoney(10000, "USD"); ImmutableMoney m2 = new ImmutableMoney(10000, "USD"); assertTrue(m1.equals(m2)); }

Perhatikan semantik ujian ini, kami menjangkakan ia akan lulus ketika kedua objek wang diuji sama dengan kaedah sama .

4. Mengapa Nilai Auto?

Now that we thoroughly understand value-types and why we need them, we can look at AutoValue and how it comes into the equation.

4.1. Issues With Hand-Coding

When we create value-types like we have done in the preceding section, we will run into a number of issues related to bad design and a lot of boilerplate code.

A two field class will have 9 lines of code: one for package declaration, two for the class signature and its closing brace, two for field declarations, two for constructors and its closing brace and two for initializing the fields, but then we need getters for the fields, each taking three more lines of code, making six extra lines.

Overriding the hashCode() and equalTo(Object) methods require about 9 lines and 18 lines respectively and overriding the toString() method adds another five lines.

That means a well-formatted code base for our two field class would take about 50 lines of code.

4.2 IDEs to The Rescue?

This is is easy with an IDE like Eclipse or IntilliJ and with only one or two value-typed classes to create. Think about a multitude of such classes to create, would it still be as easy even if the IDE helps us?

Fast forward, some months down the road, assume we have to revisit our code and make amendments to our Money classes and perhaps convert the currency field from the String type to another value-type called Currency.

4.3 IDEs Not Really so Helpful

An IDE like Eclipse can't simply edit for us our accessor methods nor the toString(), hashCode() or equals(Object) methods.

This refactoring would have to be done by hand. Editing code increases the potential for bugs and with every new field we add to the Money class, the number of lines increases exponentially.

Recognizing the fact that this scenario happens, that it happens often and in large volumes will make us really appreciate the role of AutoValue.

5. AutoValue Example

The problem AutoValue solves is to take all the boilerplate code that we talked about in the preceding section, out of our way so that we never have to write it, edit it or even read it.

We will look at the very same Money example, but this time with AutoValue. We will call this class AutoValueMoney for the sake of consistency:

@AutoValue public abstract class AutoValueMoney { public abstract String getCurrency(); public abstract long getAmount(); public static AutoValueMoney create(String currency, long amount) { return new AutoValue_AutoValueMoney(currency, amount); } }

What has happened is that we write an abstract class, define abstract accessors for it but no fields, we annotate the class with @AutoValue all totalling to only 8 lines of code, and javac generates a concrete subclass for us which looks like this:

public final class AutoValue_AutoValueMoney extends AutoValueMoney { private final String currency; private final long amount; AutoValue_AutoValueMoney(String currency, long amount) { if (currency == null) throw new NullPointerException(currency); this.currency = currency; this.amount = amount; } // standard getters @Override public int hashCode() { int h = 1; h *= 1000003; h ^= currency.hashCode(); h *= 1000003; h ^= amount; return h; } @Override public boolean equals(Object o) { if (o == this) { return true; } if (o instanceof AutoValueMoney) { AutoValueMoney that = (AutoValueMoney) o; return (this.currency.equals(that.getCurrency())) && (this.amount == that.getAmount()); } return false; } }

We never have to deal with this class directly at all, neither do we have to edit it when we need to add more fields or make changes to our fields like the currency scenario in the previous section.

Javac will always regenerate updated code for us.

While using this new value-type, all callers see is only the parent type as we will see in the following unit tests.

Here is a test that verifies that our fields are being set correctly:

@Test public void givenValueTypeWithAutoValue_whenFieldsCorrectlySet_thenCorrect() { AutoValueMoney m = AutoValueMoney.create("USD", 10000); assertEquals(m.getAmount(), 10000); assertEquals(m.getCurrency(), "USD"); }

A test to verify that two AutoValueMoney objects with the same currency and same amount test equal follow:

@Test public void given2EqualValueTypesWithAutoValue_whenEqual_thenCorrect() { AutoValueMoney m1 = AutoValueMoney.create("USD", 5000); AutoValueMoney m2 = AutoValueMoney.create("USD", 5000); assertTrue(m1.equals(m2)); }

When we change the currency type of one money object to GBP, the test: 5000 GBP == 5000 USD is no longer true:

@Test public void given2DifferentValueTypesWithAutoValue_whenNotEqual_thenCorrect() { AutoValueMoney m1 = AutoValueMoney.create("GBP", 5000); AutoValueMoney m2 = AutoValueMoney.create("USD", 5000); assertFalse(m1.equals(m2)); }

6. AutoValue With Builders

The initial example we have looked at covers the basic usage of AutoValue using a static factory method as our public creation API.

Notice that if all our fields were Strings, it would be easy to interchange them as we passed them to the static factory method, like placing the amount in the place of currency and vice versa.

This is especially likely to happen if we have many fields and all are of String type. This problem is made worse by the fact that with AutoValue, all fields are initialized through the constructor.

To solve this problem we should use the builder pattern. Fortunately. this can be generated by AutoValue.

Our AutoValue class does not really change much, except that the static factory method is replaced by a builder:

@AutoValue public abstract class AutoValueMoneyWithBuilder { public abstract String getCurrency(); public abstract long getAmount(); static Builder builder() { return new AutoValue_AutoValueMoneyWithBuilder.Builder(); } @AutoValue.Builder abstract static class Builder { abstract Builder setCurrency(String currency); abstract Builder setAmount(long amount); abstract AutoValueMoneyWithBuilder build(); } }

The generated class is just the same as the first one but a concrete inner class for the builder is generated as well implementing the abstract methods in the builder:

static final class Builder extends AutoValueMoneyWithBuilder.Builder { private String currency; private long amount; Builder() { } Builder(AutoValueMoneyWithBuilder source) { this.currency = source.getCurrency(); this.amount = source.getAmount(); } @Override public AutoValueMoneyWithBuilder.Builder setCurrency(String currency) { this.currency = currency; return this; } @Override public AutoValueMoneyWithBuilder.Builder setAmount(long amount) { this.amount = amount; return this; } @Override public AutoValueMoneyWithBuilder build() { String missing = ""; if (currency == null) { missing += " currency"; } if (amount == 0) { missing += " amount"; } if (!missing.isEmpty()) { throw new IllegalStateException("Missing required properties:" + missing); } return new AutoValue_AutoValueMoneyWithBuilder(this.currency,this.amount); } }

Notice also how the test results don't change.

If we want to know that the field values are actually correctly set through the builder, we can execute this test:

@Test public void givenValueTypeWithBuilder_whenFieldsCorrectlySet_thenCorrect() { AutoValueMoneyWithBuilder m = AutoValueMoneyWithBuilder.builder(). setAmount(5000).setCurrency("USD").build(); assertEquals(m.getAmount(), 5000); assertEquals(m.getCurrency(), "USD"); }

To test that equality depends on internal state:

@Test public void given2EqualValueTypesWithBuilder_whenEqual_thenCorrect() { AutoValueMoneyWithBuilder m1 = AutoValueMoneyWithBuilder.builder() .setAmount(5000).setCurrency("USD").build(); AutoValueMoneyWithBuilder m2 = AutoValueMoneyWithBuilder.builder() .setAmount(5000).setCurrency("USD").build(); assertTrue(m1.equals(m2)); }

And when the field values are different:

@Test public void given2DifferentValueTypesBuilder_whenNotEqual_thenCorrect() { AutoValueMoneyWithBuilder m1 = AutoValueMoneyWithBuilder.builder() .setAmount(5000).setCurrency("USD").build(); AutoValueMoneyWithBuilder m2 = AutoValueMoneyWithBuilder.builder() .setAmount(5000).setCurrency("GBP").build(); assertFalse(m1.equals(m2)); }

7. Conclusion

In this tutorial, we have introduced most of the basics of Google's AutoValue library and how to use it to create value-types with a very little code on our part.

An alternative to Google's AutoValue is the Lombok project – you can have a look at the introductory article about using Lombok here.

Pelaksanaan penuh semua contoh dan coretan kod ini terdapat di projek AutoValue GitHub.