{{announcement.body}}
{{announcement.title}}

Reducing Boilerplate Code With Annotations

DZone 's Guide to

Reducing Boilerplate Code With Annotations

Learn more about how Lombok annotations can reduce boilerplate code in your applications.

· Java Zone ·
Free Resource

One of the most common criticisms of Java is its verbosity — and rightfully so. Not only can it be tedious to write repetitive methods, such as getters and setters, but it can be difficult to write equals and hashCode methods correctly. Each of these methods has the same general structure, but it depends on the particular fields of a class. For example, writing getters for a class requires that the following structure be repeated for each field:

private T field;

public T getField() {
    return field;
}


While most Integrated Development Environments (IDEs) include tools for automatically generating these methods, this still does not solve some of the more egregious problems. For example, when adding a new field to a class, the author of that class must remember to generate a new getter for the field (if all fields are expected to have a getter). This problem is exacerbated when implementing hashCode methods, which depend on the order of the fields in a class.

To resolve these issues of boilerplate code, the Lombok Project has devised a set of annotations that can be used to automatically generate many of the standard methods associated with Java classes. This technique differs significantly from the IDE approach: Lombok generates bytecode at compile-time, removing the need for a developer to manually generate source code to be compiled at a later time. In this article, we will cover some of the basic features of Lombok, including generating getters, setters, equals, and hashCode methods. We will also cover some of the under-the-hood details of how Lombok accomplishes this through simple annotation processing. While there are countless other features supported by Lombok, comprehensively covering each would be prohibitive, and instead, resources are provided at the end of this article for the interested reader to learn more about the various annotations supported by Lombok.

Replacing Boilerplate Code

The purpose of Lombok is to replace the annoyance and fragility of manually defining monotonous methods and instead generate these methods during compilation. Beyond this reduction in manual programming, Lombok allows developers to remove much of the clutter associated with common class methods. For example, it is easy to lose sight of the purpose and responsibility of classes that resemble the following:

@FunctionalInterface
public interface Database<T> {
    public T getById(long id);
}

public class Service<T> {

    private final long id;
    private final String name;
    private final Database<T> database;

    public Service(long id, String name, Database<T> database) {
        this.id = id;
        this.name = name;
        this.database = database;
    }

    public T getById(long id) {
        return database.getById(id);
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Database<T> getDatabase() {
        return database;
    }

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Service)) {
            return false;
        }
        else {
            Service other = (Service) obj;
            return Objects.equals(database, other.database)
                && id == other.id
                && name.equals(name);
        }
    }

    @Override
    public String toString() {
        return "Service(id=" + id + ", name=" + name + ", database=" + database + ")";
    }
}


The purpose of the Service class is to act as a proxy for the Database class associated with some storable type, T, but due to the overwhelming number of boilerplate lines, its true purpose is easily obscured. Using Lombok, we can reduce this same class to the following:

@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {

    private final long id;
    private final String name;
    private final Database<T> database;

    public T getById(long id) {
        return database.getById(id);
    }
}


We can then test this Service class using the following snippet:

Service<Object> service = new Service<Object>(1, "SomeService", id -> { return null; });
System.out.println("Name of service: " + service.getName());


While this may look like a sleight of hand — since the Service constructor and getName method does not exist in our code — the expected methods are generated during compilation, so long as the Lombok JAR is present on the classpath. For example, if the Service class was present in a Maven application, we would need to add the following dependency to the pom.xml file (using 1.18.8, the latest version at the time of writing; check MVN Repository for latest version):

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
    <scope>provided</scope>
</dependency>


We can then build the JAR, containing our Database, Service, and test snippet, using the following command:

mvn clean package


If we execute our generated JAR, we see the following output:

Name of service: SomeService


As we will see shortly, the Lombok library processed our annotations and generated corresponding bytecode, allowing us to call methods that did not otherwise exist (namely, the Service constructor and getName). To verify that these methods were properly generated, we can inspect the compiled Service.class file within our generated JAR using the javap -c Service.class command:

public class com.dzone.albanoj2.lombok.Service<T> {
  public T getById(long);
    Code:
       ...

  public long getId();
    Code:
       ...

  public java.lang.String getName();
    Code:
       ...

  public com.dzone.albanoj2.lombok.Database<T> getDatabase();
    Code:
       ...

  public Service(long, java.lang.String, com.dzone.albanoj2.lombok.Database<T>);
    Code:
       ...

  public boolean equals(java.lang.Object);
    Code:
       ...

  protected boolean canEqual(java.lang.Object);
    Code:
       ...

  public int hashCode();
    Code:
       ...

  public java.lang.String toString();
    Code:
       ...
}


As we can see in the bytecode above, our Service class has a getter for each of its fields, a constructor that includes parameters for each of its required fields, an equals method, a canEqual method, a hashCode method, and a toString method. Each of these constructors and method has a particular meaning, mirroring their manually programmed analogs.

@Getter

This annotation generates a getter method for each of the fields in the annotated class. For example, applying this annotation to the Service class generates bytecode that is effectively equal to the following source code:

public long getId() {
    return id;
}

public String getName() {
    return name;
}

public Database<T> getDatabase() {
    return database;
}


This can be verified by inspecting the bytecode for each of the getters, which performs a simple getfield for each of the fields in the Service class:

public long getId();
  Code:
     0: aload_0
     1: getfield      #13                 // Field id:J
     4: lreturn

public java.lang.String getName();
  Code:
     0: aload_0
     1: getfield      #14                 // Field name:Ljava/lang/String;
     4: areturn

public Database<T> getDatabase();
  Code:
     0: aload_0
     1: getfield      #1                  // Field database:Lcom/dzone/albanoj2/lombok/Database;
     4: areturn


Additionally, if only certain fields should have getters, the @Getter annotation can be removed from the class level and applied to individual fields. For example, if only the name field of the Service class should have a getter, the Service class could be rewritten as:

@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {

    private final long id;
    @Getter private final String name;
    private final Database<T> database;

    public T getById(long id) {
        return database.getById(id);
    }
}


An explicit getter can also be added to the class, which keeps Lombok from generating a getter (it refers to the explicit getter). For example, we can create the following class:

@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {

    private final long id;
    private final String name;
    private final Database<T> database;

    public T getById(long id) {
        return database.getById(id);
    }

    public String getName() {
        return name + "extra";
    }
}


Looking at the bytecode generated for the Service class, we can see that the getter we explicitly created for name is maintained, while the default getter is generated for id and database:

public java.lang.String getName();
  Code:
     0: new           #3                  // class java/lang/StringBuilder
     3: dup
     4: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
     7: aload_0
     8: getfield      #5                  // Field name:Ljava/lang/String;
    11: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    14: ldc           #7                  // String extra
    16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    19: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    22: areturn

public long getId();
  Code:
     0: aload_0
     1: getfield      #9                  // Field id:J
     4: lreturn

public com.dzone.albanoj2.lombok.Database<T> getDatabase();
  Code:
     0: aload_0
     1: getfield      #1                  // Field database:Lcom/dzone/albanoj2/lombok/Database;
     4: areturn


If needed, we can also change the access level of the getters by supplying a value of the AccessLevel enumeration to the @Getter annotation (by default, the access level is set to public). For example, we could specify that the getters for the Service class should be protected, rather than public:

@Getter(AccessLevel.PROTECTED)
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {

    private long id;
    private final String name;
    private final Database<T> database;

    public T getById(long id) {
        return database.getById(id);
    }
}


Looking at the bytecode for the Service class, we see that this access level is reflected in the getters generated for the Service class:

protected long getId();
  Code:
     0: aload_0
     1: getfield      #3                  // Field id:J
     4: lreturn

protected java.lang.String getName();
  Code:
     0: aload_0
     1: getfield      #4                  // Field name:Ljava/lang/String;
     4: areturn

protected com.dzone.albanoj2.lombok.Database<T> getDatabase();
  Code:
     0: aload_0
     1: getfield      #1                  // Field database:Lcom/dzone/albanoj2/lombok/Database;
     4: areturn


@Setter

Similar to generating getters, Lombok can also generate setters. In the case of our Service class, no setters that can be generated, since all fields are final. For demonstration, we can remove the final modifier from the id field, which will allow us to create a setter for it. Once this modifier has been removed, we can apply the @Setter annotation to the Service class:

@Getter
@Setter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {

    private long id;
    private final String name;
    private final Database<T> database;

    public T getById(long id) {
        return database.getById(id);
    }
}


Lombok will now generate a setter for the id field only. If we look at the bytecode for the Service class, we see that this setter does what we expect from a setter: Assign the parameter value to the field — in this case, id — associated with the setter:

public void setId(long);
  Code:
     0: aload_0
     1: lload_1
     2: putfield      #3                  // Field id:J
     5: return


As with the @Getter annotation, we can also specify the access level of the generated setter methods by supplying a value of the AccessLevel enumeration to the @Setter annotation. We can also apply the @Setter annotation to individual fields, rather than the entire class, if only certain fields should have a setter method.

@RequiredArgsConstructor

Since we have marked each of the fields in the Service class final, they must be set through the Service constructor (in actuality, any constructor declared for Service). By using the @RequiredArgsConstructor, Lombok generates a constructor that has parameters for each of the required (final) fields and sets each of the fields to its corresponding parameter. This effectively generates the following for our Service class:

public Service(long id, String name, Database<T> database) {
    this.id = id;
    this.name = name;
    this.database = database;
}


We can verify this by inspecting the bytecode generated for the required argument constructor, which calls the Object default constructor (java/lang/Object." ":()V ) — as required by all classes, since all classes without explicit superclasses are subclasses of Object — and then sets each of the fields by performing a simple putfield:

public com.dzone.albanoj2.lombok.Service(long, java.lang.String, com.dzone.albanoj2.lombok.Database<T>);
  Code:
     0: aload_0
     1: invokespecial #5                  // Method java/lang/Object."<init>":()V
     4: aload_0
     5: lload_1
     6: putfield      #3                  // Field id:J
     9: aload_0
    10: aload_3
    11: putfield      #4                  // Field name:Ljava/lang/String;
    14: aload_0
    15: aload         4
    17: putfield      #1                  // Field database:Lcom/dzone/albanoj2/lombok/Database;
    20: return


@AllArgsConstructor

Similar to the @RequiredArgsConstructor annotation, the @AllArgsConstructor annotation generates a constructor that includes parameters for all fields, not just those with the final modifier. In the case of our Service class, the @RequiredArgsConstructor and @AllArgsConstructor will produce the same constructor, since all fields are final, but change the constructor generated through the @RequiredArgsConstructor annotation by making the id field mutable:

@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {

    private long id;
    private final String name;
    private final Database<T> database;

    public T getById(long id) {
        return database.getById(id);
    }
}


Using this updated Service implementation, the @RequiredArgsConstructor will generate a constructor of the following form:

public Service(String name, Database<T> database) {
    this.name = name;
    this.database = database;
}


On the other hand, the @AllArgsConstructor will generate a constructor of the following form:

public Service(long id, String name, Database<T> database) {
    this.id = id;
    this.name = name;
    this.database = database;
}


Inspecting the bytecode generated for the @AllArgsConstructor annotation, we see that it is identical to that of the constructor generated for @RequiredArgsConstructor when all fields were final:

public com.dzone.albanoj2.lombok.Service(long, java.lang.String, com.dzone.albanoj2.lombok.Database<T>);
  Code:
     0: aload_0
     1: invokespecial #5                  // Method java/lang/Object."<init>":()V
     4: aload_0
     5: lload_1
     6: putfield      #3                  // Field id:J
     9: aload_0
    10: aload_3
    11: putfield      #4                  // Field name:Ljava/lang/String;
    14: aload_0
    15: aload         4
    17: putfield      #1                  // Field database:Lcom/dzone/albanoj2/lombok/Database;
    20: return


@NoArgsConstructor

The last of the constructor options we can generate is the default — or no-arg — constructor. Since we are generating constructors for our Service class, a default constructor will not be created by the compiler. Instead, we have to generate one ourselves. In the case of our Service class, we cannot apply the @NoArgsConstructor annotation, since we have final fields, or the following compilation error will occur when we attempt to build our application:

variable id might not have been initialized


This can be remedied by removing the final modifier from all fields and applying the @NoArgsConstructor annotation to the class:

@Getter
@NoArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {

    private long id;
    private String name;
    private Database<T> database;

    public T getById(long id) {
        return database.getById(id);
    }
}


Lombok will then generate a constructor of the following form:

public Service() {}


We can confirm this by inspecting the bytecode generated for our Service class, where the generated no-args constructor simply calls the Object constructor:

public com.dzone.albanoj2.lombok.Service();
  Code:
     0: aload_0
     1: invokespecial #42                 // Method java/lang/Object."<init>":()V
     4: return


@EqualsAndHashCode

Equality is an essential aspect of a class definition — whether creating a value object or leveraging the equality semantics built into Java, such as adding an element to a Set — but it can be tricky to implement correctly. Additionally, Java expects that the equals and hashCode methods follow a standard set of guidelines when defining the equality for a class, such as equal objects always producing equal hashCode values (see Items 10 and 11 in Effective Java, 3rd Edition). For example, according to the Object class documentation, the equals method must have the following characteristics:

  • It is reflexive: for any non-null reference value x, x.equals(x) should return true.
  • It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
  • It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
  • For any non-null reference value x, x.equals(null) should return false.

In most cases, our IDE can be used to generate these methods, but this can be troublesome, as adding new fields requires that the existing equals and hashCode methods are regenerated, and apart of futureproofing, the code generated by IDEs can be challenging to understand. For example, given our definition of the Service class, the following are the equals and hashCode methods generated by Eclipse:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((database == null) ? 0 : database.hashCode());
    result = prime * result + (int) (id ^ (id >>> 32));
    result = prime * result + ((name == null) ? 0 : name.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;
    Service other = (Service) obj;
    if (database == null) {
        if (other.database != null)
            return false;
    } else if (!database.equals(other.database))
        return false;
    if (id != other.id)
        return false;
    if (name == null) {
        if (other.name != null)
            return false;
    } else if (!name.equals(other.name))
        return false;
    return true;
}


The use of primes in the hashCode method is correct, as it imposes order when comparing the fields — such that if an object has two fields, a and b, of the same type, a of the first object equaling b of the second object and b of the first object equaling a of the second object, do not result in the same hashCode value. Likewise, in the equals method, there are various checks for the type of the object being compared to and numerous checks that the fields of both objects are not null.

This code is required to create a correct definition of equality, but it clutters our code with intricate implementation details. Instead, it is preferable to denote that our class should have equals and hashCode methods generated and specify only which fields should be included. We can do this using the @EqualsAndHashCode annotation in Lombok:

@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {

    private final long id;
    private final String name;
    private final Database<T> database;

    public T getById(long id) {
        return database.getById(id);
    }
}


Apart from generating equals and hashCode methods, Lombok also generates a canEquals method that is used for proper equality comparisons with classes that are subclasses of classes other than Object (for more information on canEquals, see How to Write an Equality Method in Java). Note that if a getter is available for a field, the value returned from the getter will be used in the equality methods rather than the field value directly.

Additionally, fields can be excluded from the equality comparison — in both the equals and hashCode methods — by annotating a field with @EqualsAndHashCode.Exclude. For example, if we did not want the database field to be included in the equality comparison of our Service class, we could do the following:

@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {

    private final long id;
    private final String name;
    @EqualsAndHashCode.Exclude private final Database<T> database;

    public T getById(long id) {
        return database.getById(id);
    }
}


Note that equality can become tricky when dealing with superclasses. Being that the semantics of the equality comparison is abstracted behind Lombok, the @EqualsAndHashCode annotation includes various flags to allow developers to dictate the behavior of the generated equals and hashCode methods in a non-trivial type hierarchy. For more information, see the official @EqualsAndHashCode documentation.

@ToString

Overriding the toString method for a class is an important task — to ensure that objects are printed in a human-readable manner — but doing so can be tedious. In general, toString methods follow the same pattern: The name of the class (without its package) followed by the current value of the fields of the object, usually prepended by the name of the field. Since this pattern is repeated, IDEs can generate a boilerplate implementation of toString; while IDEs do have shortcuts for generating implementations of the toString method, the generated method must be updated each time the fields of the class change. Additionally, the generated toString method also introduces clutter in the class definition, which reduces the readability of the class as a whole.

To reduce the clutter, Lombok includes the @ToString annotation, which instructs the Lombok framework to generate a toString implementation on our behalf. By default, the implementation includes the name of the class and the current value of each field (with the name of the field). For example, we can create the following class:

@RequiredArgsConstructor
@ToString
public class Service<T> {

    private final long id;
    private final String name;
    private final Database<T> database;

    // ... constructors and methods ...
}

public class Database<T> {
    // ... implementation ...
}

public class Foo {}


Instantiating an object using new Service (1L, "blah", id -> null) and calling toString on that object results in the following output (the package and ID of the Database<T> lambda expression will vary depending on the project structure and execution):

Service(id=1, name=blah, database=com.dzone.albanoj2.lombok.Main$$Lambda$1/0x0000000800060c40@12f40c25)


It is important to note that if a getter is provided for a field, the getter will be used to print the field rather the value of the field. This use of getters means we manipulate the value of a field before it is printed to using the toString method generated by Lombok. For example, we can create the following class:

@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {

    private final long id;
    private final String name;
    private final Database<T> database;

    public T getById(long id) {
        return database.getById(id);
    }

    public String getName() {
        return name + " plus extra";
    }
}


Instantiating and calling toString on the object again results in the following output:

Service(id=1, name=blah plus extra, database=com.dzone.albanoj2.lombok.Main$$Lambda$1/0x0000000800060c40@12f40c25)


To exclude a field from the toString implementation, the @ToString.Exclude annotation can be applied to the desired field. For example, we could create the following class:

@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {

    private final long id;
    private final String name;

    @ToString.Exclude
    private final Database<T> database;

    public T getById(long id) {
        return database.getById(id);
    }
}


Instantiating an object and then calling the toString on that object results in the following output:

Service(id=1, name=blah)


Apart from excluding fields, the @ToString annotation includes other features, such as:

  • Applying the onlyExplicitlyIncluded flag (i.e.@ToString(onlyExplicitlyIncluded = true)) instructs Lombok to include only those fields that are explicitly annotated with @ToString.Include
  • Applying the callSuper flag (i.e., @ToString(callSuper = true)) instructs Lombok to include the toString output from the superclass as the first field, with the field name super (if configured to include the field name—see below)
  • Applying the name field to the @ToString.Include annotation (i.e., @ToString.Include(name = "something")) instructs Lombok to use the explicitly provided name as the field name; for example, if @ToString.Include(name = "something") is applied to the name field above in the last Service implementation, the output would be Service(id=1, something=blah)
  • Disabling the includeFieldNames flag (i.e. @ToString(includeFieldNames = false)) instructs Lombok to exclude the field names from the toString output; for example, if the includeFieldNames flag were set to false in the last Service implementation, the output would be Service(1, blah)

For more information, see the official Lombok @ToString documentation and @ToString JavaDocs.

How it Works

As discussed in Creating Annotations in Java, annotations do not provide any logic in-and-of themselves. Instead, additional code is needed to process annotations and perform some actions based on the annotations. To aid in de-coupling the processing logic from the annotations themselves, Java introduced the Pluggable Annotation Processing API in JSR 269, which allows JARs to register annotation processors that can consume annotations and perform actions based on the annotations encountered.

In particular, Lombok includes the following contents in its META-INF/services/javax.annotation.processing.Processor file:

lombok.launch.AnnotationProcessorHider$AnnotationProcessor
lombok.launch.AnnotationProcessorHider$ClaimingProcessor


This file allows the lombok.launch.nnotationProcessor class to be registered as an annotation processor, which eventually calls the lombok.core.AnnotationProcessor class, which in turn uses the lombok.javac.api.LombokProcessor class (APT stands for Annotation Processing Tool). These processors work in tandem to inspect the annotations applied to classes, fields, and methods to generate the bytecode corresponding to the annotation. For example, if a class is decorated with the @Getter annotation, Lombok will then generate a getter for each eligible field.

While the actual annotation processing is involved, the following resources can be used to learn more about how Lombok operates under the hood:

Working With IDEs

Adding the Lombok dependency to the pom.xml file suffices to compile and run our application from the command line, but without the Lombok plugin, IDEs will display false errors — such as a missing getter for final fields — for our project. In this section, we will explore how to add the official Lombok IDE plugins to Eclipse and IntelliJ, respectively.

Eclipse

To install the Lombok Eclipse plugin, download the Lombok JAR from MVN Repository (version 1.18.8 at the time of writing) and execute the JAR using the following command:

java -jar lombok-1.18.8.jar


Doing so will start the Lombok installer, which will search for the Eclipse installations on our system. Once the installations have been found, ensure that the installations for which the Lombok plugin should be installed are checked and then click Install / Update. Doing so will install the Lombok plugin for those Eclipse installations.

Once the plugin is installed for the desired installations, restart Eclipse.

Image title

IntelliJ

To install the Lombok IntelliJ plugin, we need to first enable annotation processing in IntelliJ and then install the plugin itself. To enable annotation processing, complete the following steps:

  1. Click File
  2. Click Settings
  3. Click the Build, Execution, Deployment heading
  4. Click the Compiler heading
  5. Click Annotation Processors
  6. Check the Enable annotation processing checkbox in the right-hand panel
  7. Click the OK button at the bottom of the setting window

To install the plugin, complete the following steps:

  1. Click File
  2. Click Settings
  3. Click the Plugins heading
  4. Enter lombok into the search field
  5. Click the Install button under the Lombok (Tools integration) plugin
  6. Click the Accept button at the bottom of the Third-party Plugins Privacy Note window
  7. Click the Restart IDE button under the Lombok (Tools integration) plugin

Image title

More Information

For more information on Lombok and how to use its features, see the following:

Conclusion

Java is notorious for its verbosity, and the addition of boilerplate code only makes developing code in Java longer in the tooth. To remedy this issue, Lombok introduces various annotations that can be applied to classes and fields to generate methods, such as getters, setters, and constructors. Not only does this reduce the amount of code we must manually write, but it also removes a large portion of the clutter from our class definitions. Additionally, it also ensures that we correctly and consistently implement difficult techniques, such as equality. While Lombok is not as ubiquitous as other frameworks, in the right situations, it can greatly aid in the readability and correctness of Java code.

Topics:
java ,boilerplate code ,lombok ,getters and setters ,builder pattern ,annotations ,tutorial ,methods ,verbose

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}