Java Coding Style - raisercostin/software-wiki GitHub Wiki

References

Records

  • Use records for java16
  • java15 and smaller
    • Use minimal lombok
      • Lombok's @Data and @Value generate getters and setters that is a bad practice to have (to add link to argument).
      • Best to use just @ToString @EqualsAndHashCode based on the documentation of @Data and @Value:
        • @Data = Equivalent to @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode
        • @Value= Equivalent to @Getter @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) @AllArgsConstructor @ToString @EqualsAndHashCode
  • Kotlin - Use records
  • Scala - Use case classes

Specialized

  • Lombok, Mapstruct, JPA
  • JPA
  • Exception Handling & Audit
  • Formatting and cleanup configuration
  • Vavr lesons
    • jackson should read to java Iterable/List (vavr reads native java and then converts)
    • every step in vavr generates a new collection is not good for heap
      • preferable to use vavr.Iterator or RichIterable
    • jackson write can use Iterable for writing which is perfect. You don't need to have full structures but just to iterate on some
    • domain should be in VAVR since is imutable and can be changed while iterating over without any concurrency issues
  • Vavr - use RichIterable
    • RichIterable.fromVavr - should be used on top of any vavr.Traversable. This is just a wrapper so is O(1).
    • RichIterable.fromJava - should be used on top of any java.Collection or java.Iterable. This is just a wrapper so is O(1).
    • The operations on RichIterable are transitory so will not create intermediate vavr collections.
    • If needed a memoizeAsJava() or memoizeAsVavr will create a collection and use the iterable on that further. In this way the previous filters/maps will not be applied on each iterator.
    • Sorting/reversing and other operations will be transfered in java collections that are faster than vavr.
    • In the future we might change the RichIterable implementation to be based on
    • See more on https://github.com/lacuna/bifurcan/blob/master/doc/comparison.md
  • Vavr & jackson
    • jackson library reads java lists then converts to vavr collections. Let's keep the java collections better or Iterable?
  • Doubles - see https://github.com/raisercostin/revomatico/wiki/Data-Structures
    • search, remove or replace with BigDecimal any reference to
      • double and Double field - replace with BigDecimal
      • doubleValue() - conversion to double
      • new Double(value) - with BigDecimal.valueOf(value)
      • Double.parseDouble(stringValue) - with new BigDecimal(stringValue)
      • com.google.common.math.DoubleMath
      • comparing double using == or equals or comparator with the expectation to get a 0 in case of equality
      • (!warning!) comparing Double or BigDecimal must be make with an epsilon/precision
  • Antipatterns
    • sorted follwed by reverse or reverseOrder
      • .sorted(selector).reverse() => .sorted(comparator.reverse())
    • sortBy followed by reverse
      • .sortBy(selector).reverse() => .sorted(Comparator.comparing((T outcome) -> selector).reversed())
      • .sortBy(selector).reverse() => .sortBy(-selector) if is possible
    • sortBy followed by sortBy - use a single sort(comparator)
    • RichIterable has a sortByReversed
      • .sortBy(selector).reverse() => (RichIterable)list.sortByReversed(selector)
  • Libraries
    • async-http-client library
      • WebClient (or WebClientLocation that is the jedio wrapper) that is very good async library
      • Do Not Use
        • Unirest, apache-http-client (sync/async), fibers

Lombok, Mapstruct, JPA

There are some issues when combining these technologies.

Advice

  • Do not pass to hibernate vavr.Seq.asJava() since the collection is not mutable. Use vavr.Seq.toJavaList()

  • The mapper can create objects

    • forced via constructors instead builder - @Mapper(config = GotappMapstructConfig.class, builder = @Builder(disableBuilder = true)) public interface InsisMapper {
    • using no args constructor
    • using all args constructor
  • This is what we use in revobet.core.domain.player.BetslipDetail class

//Lombok & JPA
// - this sample: https://stackoverflow.com/a/60900881/99248
// https://stackoverflow.com/questions/34241718/lombok-builder-and-jpa-default-constructor

//Lombok
//Mandatory in conjunction with JPA: an equal based on fields is not desired
@lombok.EqualsAndHashCode(onlyExplicitlyIncluded = true)
//Mandatory in conjunction with JPA: force is needed to generate default values for final fields, that will be overriden by JPA
@lombok.NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
//Hides the constructor to force usage of the Builder.
@lombok.AllArgsConstructor(access = AccessLevel.PRIVATE)
@lombok.ToString
//Good to just modify some values
@lombok.With
//Mandatory in conjunction with JPA: Some suggest that the Builder should be above Entity - https://stackoverflow.com/a/52048267/99248
//Good to be used to modify all values
@lombok.Builder(toBuilder = true)
//final fields needed for imutability, the default access to public - since are final is safe 
@lombok.experimental.FieldDefaults(makeFinal = true, level = AccessLevel.PUBLIC)
//no getters and setters
@lombok.Getter(value = AccessLevel.NONE)
@lombok.Setter(value = AccessLevel.NONE)

//JPA
@javax.persistence.Entity
@javax.persistence.Table(name = "BET_SLIP_DETAIL")
//jpa should use field access 
@javax.persistence.Access(AccessType.FIELD)
public class BetSlipDetail {
  @javax.persistence.Id
  @javax.persistence.GeneratedValue
  //Used also automatically as JPA
  @lombok.EqualsAndHashCode.Include
  Long id;
  Long betId;
  String matchId;
  String marketCode;
  • Adding @lombok.Builder(toBuilder = true) will be used by mapstruct but the @AfterMapping will need to use the LeagueUpdate.LeagueUpdateBuilder lombok generated class. The problem shows up if you want to call some operations on original LeagueUpdate class see validateFromFeed():
  @Deprecated
  @Mapping(target = "leagueCodeFromFeed", source = "league.id")
  @Mapping(target = "leagueName", source = "league.name")
  @Mapping(target = "locationCodeFromFeed", source = "league.country")
  @Mapping(target = "locationName", source = "league.country")
  @Mapping(target = "season", source = "league.season")
  @Mapping(target = "start", source = "league.dateStart")
  @Mapping(target = "end", source = "league.dateEnd")
  @Mapping(target = "source", expression = "java(league)")
  LeagueUpdate toLeague(GSLeague league, @Context GSSport context);

  @AfterMapping
  default void toLeagueAfter(@MappingTarget /*LeagueUpdate*/LeagueUpdate.LeagueUpdateBuilder leagueUpdate, GSLeague league,
      @Context GSSport sport) {
    leagueUpdate.sportCodeFromFeed = sport.id + "";
    leagueUpdate.sportName = sport.name;
    leagueUpdate.leagueCodeFromFeed = league.id;
    //leagueUpdate.validateFromFeed(); - cannot be called on builder
  }

lombok with wither

Also see https://github.com/rzwitserloot/lombok/issues/2150

I'm using lombok-1.18.12. I was able to initialize the fields with

class LombokTest {
  @lombok.Value
  @lombok.NoArgsConstructor(force = true)
  @lombok.AllArgsConstructor(access = AccessLevel.PRIVATE)
  @lombok.With
  @lombok.Builder(access = AccessLevel.PRIVATE)
  //@lombok.experimental.FieldDefaults(makeFinal = true, level = AccessLevel.PUBLIC)
  //@lombok.Getter(value = AccessLevel.NONE)
  //@lombok.Setter(value = AccessLevel.NONE)
  public static class Point {
    @lombok.Builder.Default
    private final int x = 3;
    private final int y;
  }

  @Test
  void testWithers() {
    assertThat(new Point().withY(2).toString()).isEqualTo("NodesTest.Point(x=3, y=2)");
    assertThat(new Point().withX(5).withY(2).toString()).isEqualTo("NodesTest.Point(x=5, y=2)");
    assertThat(new Point().withX(5).toString()).isEqualTo("NodesTest.Point(x=5, y=0)");
  }
}

image

Lombok field defaults

When using lombok @FieldDefaults(level = AccessLevel.PUBLIC, makeFinal = true) and need to override some default fields it is mandatory to also use the @Builder.Default on the targeted fields otherwise the field will not be overridden.

Also in the lombok.config file the following setting is needed:

lombok.anyConstructor.addConstructorProperties=true

If the @lombok.NoArgsConstructor is enabled then the com.fasterxml.jackson.annotation.JsonCreator should be added to one of constructors in onConstructor configuration

Using lombok-1.18.20, the expexcted behaviour would be achieved as follows:

@Slf4j
@Value
@lombok.AllArgsConstructor(access = AccessLevel.PUBLIC, onConstructor_ = { @ConstructorBinding })
//@lombok.NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
//if enabled add com.fasterxml.jackson.annotation.JsonCreator to one of constructors. see onConstructor
@lombok.With
@lombok.Builder(toBuilder = true)
@lombok.Getter(value = AccessLevel.NONE)
@lombok.Setter(value = AccessLevel.NONE)
@FieldDefaults(level = AccessLevel.PUBLIC, makeFinal = true)
@ConfigurationProperties(prefix = "lsports.kafka.preplay")
public class LsportsKafkaConfig {

  public static LsportsKafkaConfig readPreplayConfig() {
    return RichConfig.readConfig("classpath:lsports-kafka.conf", "lsports.kafka.preplay",
      LsportsKafkaConfig.class);
  }

  public String topic;
  public String brokerAddress;
  @Builder.Default
  public long bufferMemorySize = 26000002;
  @Builder.Default
  public long maxRequestSize = 25000002;
}

image

JPA

  • All time fields that are stored in database should be like this. Exceptions should be discussed. (#1784)
    @Column(columnDefinition = "TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL")
    OffsetDateTime date;
    
  • All database entities should have a created and updated field managed by database
    @Column(columnDefinition = "TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL")
    OffsetDateTime created;
    @Column(columnDefinition = "TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL")
    @UpdateTimestamp
    OffsetDateTime updated;