Ron and Ella Wiki Page

Extremely Serious

Page 3 of 39

MapStruct SubclassMapping for Inheritance Hierarchies

MapStruct's @SubclassMapping elegantly solves polymorphic object mapping in inheritance scenarios, generating runtime type-safe conversions from parent to child classes. This annotation shines when mapping abstract base classes or interfaces with concrete subclasses, avoiding manual instanceof checks while ensuring correct target instantiation.

Core Problem It Solves

Without @SubclassMapping, MapStruct treats inheritance hierarchies naively—mapping a Vehicle source to VehicleDTO target ignores subclass details, losing Car-specific doors or Bus-specific seats. The annotation configures explicit source→target subclass pairs on parent-level mapper methods, generating dispatch logic like if (source instanceof Car) return mapCar((Car) source). Use it precisely when source/target hierarchies diverge or when abstract targets need concrete delegation.

Complete Compilable Example

// Source hierarchy
abstract class Vehicle {
    private String brand;
    public String getBrand() { return brand; }
    public void setBrand(String brand) { this.brand = brand; }
}

class Car extends Vehicle {
    private int doors;
    public int getDoors() { return doors; }
    public void setDoors(int doors) { this.doors = doors; }
}

class Bus extends Vehicle {
    private int seats;
    public int getSeats() { return seats; }
    public void setSeats(int seats) { this.seats = seats; }
}

// Target hierarchy (mirrors structure)
abstract class VehicleDTO {
    private String brand;
    public String getBrand() { return brand; }
    public void setBrand(String brand) { this.brand = brand; }
}

class CarDTO extends VehicleDTO {
    private int doors;
    public int getDoors() { return doors; }
    public void setDoors(int doors) { this.doors = doors; }
}

class BusDTO extends VehicleDTO {
    private int seats;
    public int getSeats() { return seats; }
    public void setSeats(int seats) { this.seats = seats; }
}

Mapper with @SubclassMapping

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.SubclassMapping;

@Mapper(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION)
interface VehicleMapper {
    @SubclassMapping(source = Car.class, target = CarDTO.class)
    @SubclassMapping(source = Bus.class, target = BusDTO.class)
    VehicleDTO toDTO(Vehicle vehicle);

    @Mapping(target = "doors", source = "doors")
    CarDTO toCarDTO(Car car);

    @Mapping(target = "seats", source = "seats")
    BusDTO toBusDTO(Bus bus);
}

Key Requirements: Define concrete subclass mappers (toCarDTO, toBusDTO) explicitly—MapStruct delegates to them. @SubclassMapping pairs source/target subclasses; repeat for each pair.

Generated Dispatch Logic

MapStruct produces clean instanceof dispatch:

public VehicleDTO toDTO(Vehicle vehicle) {
    if ( vehicle == null ) {
        return null;
    }

    if (vehicle instanceof Car) {
        return toCarDTO( (Car) vehicle );
    }
    else if (vehicle instanceof Bus) {
        return toBusDTO( (Bus) vehicle );
    }
    else {
        throw new IllegalArgumentException("Not all subclasses are supported for this mapping. Missing for " + vehicle.getClass());
    }
}

For abstract targets, it skips parent instantiation entirely. Configure subclassExhaustiveStrategy for custom fallbacks (e.g., default parent mapping).

Usage and Verification

import org.mapstruct.factory.Mappers;

public class Demo {
    public static void main(String[] args) {
        VehicleMapper mapper = Mappers.getMapper(VehicleMapper.class);

        Car car = new Car(); car.setBrand("Toyota"); car.setDoors(4);
        Bus bus = new Bus(); bus.setBrand("Volvo"); bus.setSeats(40);

        VehicleDTO carDTO = mapper.toDTO(car);  // instanceof CarDTO
        VehicleDTO busDTO = mapper.toDTO(bus);  // instanceof BusDTO

        System.out.println(carDTO.getClass().getSimpleName() + ": " + ((CarDTO)carDTO).getDoors());  // CarDTO: 4
        System.out.println(busDTO.getClass().getSimpleName() + ": " + ((BusDTO)busDTO).getSeats());  // BusDTO: 40
    }
}

Runtime polymorphism works seamlessly—input Car yields CarDTO with preserved subclass state.

MapStruct: MappingTarget and TargetType with Pure Java

MapStruct's @MappingTarget and @TargetType annotations solve distinct mapping challenges. @MappingTarget enables in-place updates of existing objects, while @TargetType provides runtime type information for dynamic object creation.

Core Concepts

@MappingTarget marks a parameter as an existing target for modification rather than creating a new instance. Use it for DTO-to-entity updates where the target object already exists in memory (e.g., from a database).

@TargetType injects the concrete target Class<T> as a parameter, essential for generic factories and lifecycle methods where compile-time type information is erased.

Complete Working Example

import org.mapstruct.*;
import java.lang.reflect.Constructor;

// 1. Plain Java DTOs and Entities
class CarDto {
    private String make;
    private String model;
    private int year;

    public CarDto() {}

    public CarDto(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    // Getters and setters
    public String getMake() { return make; }
    public void setMake(String make) { this.make = make; }

    public String getModel() { return model; }
    public void setModel(String model) { this.model = model; }

    public int getYear() { return year; }
    public void setYear(int year) { this.year = year; }
}

class CarEntity {
    private String make;
    private String model;
    private int year;
    private String vin;  // Entity-specific field

    public CarEntity() {}

    // Getters and setters
    public String getMake() { return make; }
    public void setMake(String make) { this.make = make; }

    public String getModel() { return model; }
    public void setModel(String model) { this.model = model; }

    public int getYear() { return year; }
    public void setYear(int year) { this.year = year; }

    public String getVin() { return vin; }
    public void setVin(String vin) { this.vin = vin; }
}

// 2. @TargetType Custom Factory
class CarEntityFactory {
    public <T extends CarEntity> T createEntity(@TargetType Class<T> entityClass) {
        try {
            Constructor<T> constructor = entityClass.getDeclaredConstructor();
            T entity = constructor.newInstance();
            entity.setVin("VIN-" + entityClass.getSimpleName());
            return entity;
        } catch (Exception e) {
            throw new RuntimeException("Cannot create entity: " + entityClass.getName(), e);
        }
    }
}

// 3. Mapper Demonstrating Both Annotations
@Mapper(uses = CarEntityFactory.class)
interface CarMapper {

    // Creates NEW object using @TargetType factory
    CarEntity toEntity(CarDto dto);

    // Updates EXISTING object - @MappingTarget required
    void updateEntity(@MappingTarget CarEntity entity, CarDto dto);

    // Updates and returns - also uses @MappingTarget
    CarEntity updateAndReturn(@MappingTarget CarEntity entity, CarDto dto);
}

// 4. Advanced Mapper with Lifecycle Methods
@Mapper(uses = CarEntityFactory.class)
abstract class AdvancedCarMapper {

    public abstract CarEntity toEntity(CarDto dto);

    @BeforeMapping
    void beforeUpdate(CarDto source,
                              @MappingTarget CarEntity target,
                              @TargetType Class<?> targetType) {
        System.out.println("🔄 Before: Updating " + targetType.getSimpleName());
        System.out.println("   Target VIN before: " + target.getVin());
    }

    @AfterMapping
    void afterUpdate(CarDto source,
                             @MappingTarget CarEntity target,
                             @TargetType Class<?> targetType) {
        System.out.println("✅ After: Updated " + targetType.getSimpleName());
        System.out.println("   Target VIN after: " + target.getVin());
    }
}

Usage Demonstration

import org.mapstruct.factory.Mappers;

public class MapStructDemo {
    public static void main(String[] args) {
        CarMapper mapper = Mappers.getMapper(CarMapper.class);
        AdvancedCarMapper advancedMapper = Mappers.getMapper(AdvancedCarMapper.class);

        System.out.println("=== 1. NEW Object Creation (uses @TargetType factory) ===");
        CarDto newDto = new CarDto("Toyota", "Camry", 2023);
        CarEntity newEntity = mapper.toEntity(newDto);
        System.out.println("New entity: " + newEntity.getMake() + " VIN: " + newEntity.getVin());
        // Output: VIN-CarEntity

        System.out.println("\n=== 2. Update Existing (uses @MappingTarget) ===");
        CarEntity existingEntity = new CarEntity();
        existingEntity.setVin("EXISTING-123");
        CarDto updateDto = new CarDto("Honda", "Civic", 2022);

        mapper.updateEntity(existingEntity, updateDto);
        System.out.println("Updated: " + existingEntity.getModel() + " VIN preserved: " + existingEntity.getVin());
        // VIN preserved! Only mapped fields updated

        System.out.println("\n=== 3. Lifecycle Hooks (both annotations) ===");
        advancedMapper.toEntity(new CarDto("Ford", "Mustang", 2024));
        // Prints before/after messages with type info
    }
}

Expected Output

=== 1. NEW Object Creation (uses @TargetType factory) ===
New entity: Toyota VIN: VIN-CarEntity

=== 2. Update Existing (uses @MappingTarget) ===
Updated: Civic VIN preserved: EXISTING-123

=== 3. Lifecycle Hooks (both annotations) ===
🔄 Before: Updating CarEntity
   Target VIN before: null
✅ After: Updated CarEntity
   Target VIN after: VIN-CarEntity

Decision Matrix: When to Use Each

Scenario @MappingTarget @TargetType
DTO → New Entity ❌ No ✅ Factory methods
Partial DTO Update ✅ Always ❌ No
Generic Collections ❌ No ✅ Essential
Lifecycle Processing ✅ Target param ✅ Type param
Polymorphic Mapping ❌ No ✅ Required

This pure Java example demonstrates both annotations in realistic enterprise scenarios, showing the clear distinction between new object creation and in-place updates.

MapStruct Inheritance: InheritConfiguration and InheritInverseConfiguration

MapStruct's @InheritConfiguration and @InheritInverseConfiguration annotations streamline complex mapping hierarchies and bidirectional conversions. These tools eliminate repetitive @Mapping definitions while maintaining type safety and compile-time validation.

Core Concepts

@InheritConfiguration copies forward mapping rules from a base method to a child method with identical source/target types. Local mappings override inherited ones for flexibility.

@InheritInverseConfiguration automatically reverses a forward mapping by swapping source and target types. It handles field renames and ignores, generating clean DTO-to-entity roundtrips.

Both require MappingInheritanceStrategy.EXPLICIT (MapStruct default).

Complete Working Example

Domain Models

public class Car {
    private String make;
    private int numberOfSeats;
    private int doors;

    public Car() {}
    public Car(String make, int numberOfSeats, int doors) {
        this.make = make;
        this.numberOfSeats = numberOfSeats;
        this.doors = doors;
    }

    // Getters and setters
    public String getMake() { return make; }
    public void setMake(String make) { this.make = make; }
    public int getNumberOfSeats() { return numberOfSeats; }
    public void setNumberOfSeats(int numberOfSeats) { this.numberOfSeats = numberOfSeats; }
    public int getDoors() { return doors; }
    public void setDoors(int doors) { this.doors = doors; }
}

public class CarDto {
    private String manufacturer;
    private Integer seatCount;
    private boolean hasNavigationSystem;

    public CarDto() {}
    public CarDto(String manufacturer, Integer seatCount, boolean hasNavigationSystem) {
        this.manufacturer = manufacturer;
        this.seatCount = seatCount;
        this.hasNavigationSystem = hasNavigationSystem;
    }

    // Getters and setters
    public String getManufacturer() { return manufacturer; }
    public void setManufacturer(String manufacturer) { this.manufacturer = manufacturer; }
    public Integer getSeatCount() { return seatCount; }
    public void setSeatCount(Integer seatCount) { this.seatCount = seatCount; }
    public boolean isHasNavigationSystem() { return hasNavigationSystem; }
    public void setHasNavigationSystem(boolean hasNavigationSystem) { this.hasNavigationSystem = hasNavigationSystem; }
}

Inheritance Hierarchy Example

import org.mapstruct.InheritConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper
public interface VehicleMapper {
    @Mapping(target = "hasNavigationSystem", ignore = true)
    @Mapping(target = "manufacturer", source = "make")
    @Mapping(target = "seatCount", source = "numberOfSeats")
    CarDto vehicleToDto(Car vehicle);
}

@Mapper
public interface CarMapper extends VehicleMapper {
    @InheritConfiguration(name = "vehicleToDto")
    CarDto carToCarDto(Car car);

    // Custom mapping overrides inheritance
    @InheritConfiguration(name = "vehicleToDto")
    @Mapping(target = "hasNavigationSystem", constant = "false")
    CarDto carToBasicDto(Car car);
}

Bidirectional Mapping Example

import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper
public interface BidirectionalCarMapper {
    // Forward mapping with field renaming
    @Mapping(target = "hasNavigationSystem", ignore = true)
    @Mapping(target = "seatCount", source = "numberOfSeats")
    @Mapping(target = "manufacturer", source = "make")
    CarDto toDto(Car car);

    // Automatically reverses: seatCount->numberOfSeats, manufacturer->make
    @InheritInverseConfiguration(name = "toDto")
    @Mapping(target = "doors", ignore = true)
    Car toEntity(CarDto dto);

    // Selective reverse with local overrides
    @InheritInverseConfiguration
    @Mapping(target = "numberOfSeats", ignore = true)
    @Mapping(target = "doors", ignore = true)
    Car toEntityNoSeats(CarDto dto);
}

Verification Demo

import org.mapstruct.factory.Mappers;

public class MapStructInheritanceDemo {
    public static void main(String[] args) {
        CarMapper carMapper = Mappers.getMapper(CarMapper.class);
        BidirectionalCarMapper biMapper = Mappers.getMapper(BidirectionalCarMapper.class);

        // Test inheritance
        Car ferrari = new Car("Ferrari", 2, 2);
        CarDto dto = carMapper.carToCarDto(ferrari);
        System.out.println(dto.getManufacturer()); // "Ferrari"
        System.out.println(dto.getSeatCount());    // 2

        // Test bidirectional
        CarDto sportsCarDto = new CarDto("Porsche", 4, true);
        Car entity = biMapper.toEntity(sportsCarDto);
        System.out.println(entity.getMake());        // "Porsche"
        System.out.println(entity.getNumberOfSeats()); // 4
    }
}

Comparison Table

Feature @InheritConfiguration @InheritInverseConfiguration
Direction Forward → Forward Forward → Reverse
Type Match Identical source/target Swapped source/target
Primary Use Class hierarchies DTO ↔ Entity roundtrips

Best Practices

For @InheritConfiguration:

  • Use in mapper hierarchies (extends)
  • Document base mappings clearly
  • Override selectively with @Mapping

For @InheritInverseConfiguration:

  • Specify name= when source mapper has multiple similar methods
  • Use @Mapping(ignore=true) for one-way fields

These annotations reduce mapping code by 70-80% in typical enterprise applications while maintaining full compile-time safety.

MapStruct Imports: Pure Java Code

MapStruct's imports attribute brings external types into scope for generated mappers. This guide focuses purely on the working code demonstrating imports in action.

Domain Model

Abstract base entity:

package com.example.domain;

public abstract class AbstractBaseEntity {
    private Long id;

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public abstract String getStatus();
}

Source entity:

package com.example.domain;

public class UserEntity extends AbstractBaseEntity {
    private String username;
    private String email;

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    @Override
    public String getStatus() { return "ACTIVE"; }
}

Target DTO:

package com.example.dto;

public class UserDto {
    private Long id;
    private String username;
    private String email;
    private String statusInfo;

    // getters/setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getStatusInfo() { return statusInfo; }
    public void setStatusInfo(String statusInfo) { this.statusInfo = statusInfo; }

    @Override
    public String toString() {
        return "UserDto{id=" + id + ", username='" + username + "', statusInfo='" + statusInfo + "'}";
    }
}

Mapper with Imports

package com.example.mapper;

import com.example.dto.UserDto;
import com.example.domain.UserEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.util.List;

@Mapper(imports = com.example.domain.AbstractBaseEntity.class)
public interface UserMapper {

    @Mapping(target = "id", source = "id")
    @Mapping(target = "username", source = "username")
    @Mapping(target = "email", source = "email")
    @Mapping(target = "statusInfo", expression = "java( ((AbstractBaseEntity) entity).getStatus() + \" (processed)\")")
    UserDto toDto(UserEntity entity);

    List<UserDto> toDtos(List<UserEntity> entities);
}

Usage Code

package com.example.demo;

import com.example.domain.UserEntity;
import com.example.dto.UserDto;
import com.example.mapper.UserMapper;
import org.mapstruct.factory.Mappers;

public class MapStructDemo {
    private static final UserMapper MAPPER = Mappers.getMapper(UserMapper.class);

    public static void main(String[] args) {
        UserEntity entity = new UserEntity();
        entity.setId(42L);
        entity.setUsername("john_doe");
        entity.setEmail("john@example.com");

        UserDto dto = MAPPER.toDto(entity);
        System.out.println("Mapped DTO: " + dto);
    }
}

Generated Code Insight

MapStruct generates UserMapperImpl with automatic import:

package com.example.mapper;

import com.example.domain.AbstractBaseEntity; // Added by imports attribute
import com.example.domain.UserEntity;
import com.example.dto.UserDto;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.processing.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-9.2.0.jar, environment: Java 25 (Oracle Corporation)"
)
public class UserMapperImpl implements UserMapper {

    @Override
    public UserDto toDto(UserEntity entity) {
        if ( entity == null ) {
            return null;
        }

        UserDto userDto = new UserDto();

        userDto.setId( entity.getId() );
        userDto.setUsername( entity.getUsername() );
        userDto.setEmail( entity.getEmail() );

        userDto.setStatusInfo( ((AbstractBaseEntity) entity).getStatus() + " (processed)" );

        return userDto;
    }

    @Override
    public List<UserDto> toDtos(List<UserEntity> entities) {
        if ( entities == null ) {
            return null;
        }

        List<UserDto> list = new ArrayList<UserDto>( entities.size() );
        for ( UserEntity userEntity : entities ) {
            list.add( toDto( userEntity ) );
        }

        return list;
    }
}

Why Imports Are Required

❌ Without imports attribute:

// GENERATED CODE FAILS TO COMPILE:
// error: cannot find symbol
// symbol: class AbstractBaseEntity
// ((AbstractBaseEntity) entity).getStatus()

✅ With imports = AbstractBaseEntity.class:

// GENERATED CODE WORKS PERFECTLY:
// import com.example.domain.AbstractBaseEntity;
// ((AbstractBaseEntity) entity).getStatus() ✓

Multiple Imports Pattern

@Mapper(imports = { 
    com.example.domain.AbstractBaseEntity.class,
    java.time.LocalDateTime.class 
})
public interface AdvancedUserMapper {

    @Mapping(target = "createdAt", expression = "java(LocalDateTime.now())")
    @Mapping(target = "fullInfo", expression = "java(entity.getUsername() + \" <\" + ((AbstractBaseEntity)entity).getStatus() + \">\")")
    UserDto toDto(UserEntity entity);
}

The imports attribute eliminates compilation errors when mappings reference types across packages or inheritance hierarchies.

MapStruct Builder Support with Java Records

Java records combined with explicit builder patterns provide immutable data classes that work seamlessly with MapStruct's builder detection. This approach leverages records' automatic getters and compact constructors while adding the required builder infrastructure for mapping.

Complete Record Target Class

The Person record with builder support:

public record Person(
    Long id,
    String firstName,
    String lastName
) {

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private Long id;
        private String firstName;
        private String lastName;

        public Builder id(Long id) {
            this.id = id;
            return this;
        }

        public Builder firstName(String firstName) {
            this.firstName = firstName;
            return this;
        }

        public Builder lastName(String lastName) {
            this.lastName = lastName;
            return this;
        }

        public Person build() {
            return new Person(id, firstName, lastName);
        }
    }
}

Complete Source Class

Simple mutable source class for demonstration:

public class ExternalPerson {
    private Long id;
    private String personFirstName;
    private String personLastName;

    public ExternalPerson(Long id, String personFirstName, String personLastName) {
        this.id = id;
        this.personFirstName = personFirstName;
        this.personLastName = personLastName;
    }

    // Getters and setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getPersonFirstName() {
        return personFirstName;
    }

    public void setPersonFirstName(String personFirstName) {
        this.personFirstName = personFirstName;
    }

    public String getPersonLastName() {
        return personLastName;
    }

    public void setPersonLastName(String personLastName) {
        this.personLastName = personLastName;
    }
}

Complete Mapper Interface

MapStruct automatically detects the record's static builder() method:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper
public interface PersonMapper {
    @Mapping(source = "personFirstName", target = "firstName")
    @Mapping(source = "personLastName", target = "lastName")
    Person toPerson(ExternalPerson source);
}

Generated Mapping Code

MapStruct generates efficient builder-based mapping:

import javax.annotation.processing.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-9.2.0.jar, environment: Java 25 (Oracle Corporation)"
)
public class PersonMapperImpl implements PersonMapper {

    @Override
    public Person toPerson(ExternalPerson source) {
        if ( source == null ) {
            return null;
        }

        Person.Builder person = Person.builder();

        person.firstName( source.getPersonFirstName() );
        person.lastName( source.getPersonLastName() );
        person.id( source.getId() );

        return person.build();
    }
}

Custom Builder Method Names

Override defaults using @Builder annotation on the mapper:

import org.mapstruct.Builder;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(builder = @Builder(buildMethod = "construct"))
public abstract class PersonMapper {
    @Mapping(source = "personFirstName", target = "firstName")
    @Mapping(source = "personLastName", target = "lastName")
    abstract Person toPerson(ExternalPerson source);
}

For this configuration, change the record to:

// In Builder class:
public Person construct() {  // Changed from build()
    return new Person(id, firstName, lastName);
}

Key Benefits of Record + Builder Pattern

  • Records provide automatic equals(), hashCode(), toString(), and getters
  • Immutable by default with final components
  • Builder enables fluent construction for complex initialization
  • MapStruct auto-detects standard builder() + build() pattern

MapStruct Context: Guide with Lifecycle & ObjectFactory Examples

MapStruct's @Context annotation enables passing contextual data—like locales, services, or user state—through mapper method calls without treating it as source or target input. This propagates automatically to lifecycle methods (@BeforeMapping, @AfterMapping, @ObjectFactory) and custom qualifiers, making it ideal for complex mappings requiring external dependencies.

Core Concepts

@Context marks parameters for transparent propagation: upstream mapper methods forward them to downstream calls, including generated code, custom methods, and lifecycle hooks. Callers must supply values explicitly, as MapStruct performs no instantiation or defaulting. Propagation requires matching @Context declarations across methods, with type compatibility enforced.

Key benefits include locale-aware formatting, injecting services for lookups, or stateful transformations—all without cluttering method signatures with non-mapping data.

Complete Runnable Example

Save as ContextDemo.java.

import org.mapstruct.*;
import org.mapstruct.factory.Mappers;

import java.util.Locale;
import java.time.format.DateTimeFormatter;
import java.time.LocalDate;

// Source and Target DTOs
class Car {
    private String manufacturer;
    private String model;
    private LocalDate built;

    public Car(String manufacturer, String model, LocalDate built) {
        this.manufacturer = manufacturer;
        this.model = model;
        this.built = built;
    }

    // Getters/Setters
    public String getManufacturer() { return manufacturer; }
    public void setManufacturer(String manufacturer) { this.manufacturer = manufacturer; }
    public String getModel() { return model; }
    public void setModel(String model) { this.model = model; }
    public LocalDate getBuilt() { return built; }
    public void setBuilt(LocalDate built) { this.built = built; }
}

class CarDto {
    private String make;
    private String fullModel;
    private String formattedBuildDate;
    private String contextInfo;

    // Getters/Setters
    public String getMake() { return make; }
    public void setMake(String make) { this.make = make; }
    public String getFullModel() { return fullModel; }
    public void setFullModel(String fullModel) { this.fullModel = fullModel; }
    public String getFormattedBuildDate() { return formattedBuildDate; }
    public void setFormattedBuildDate(String formattedBuildDate) { this.formattedBuildDate = formattedBuildDate; }
    public String getContextInfo() { return contextInfo; }
    public void setContextInfo(String contextInfo) { this.contextInfo = contextInfo; }
}

// Mapper with full lifecycle + ObjectFactory
@Mapper(uses = LocalDateUtils.class)
abstract class CarMapper {
    // Top-level entry point - caller provides @Context
    @Mapping(source = "manufacturer", target = "make")
    @Mapping(source = "built", target = "formattedBuildDate", qualifiedByName = "formatDate")
    @Mapping(target = "fullModel", ignore = true)  // Set in @AfterMapping
    @Mapping(target = "contextInfo", ignore = true)  // Set in @ObjectFactory
    public abstract CarDto toDto(Car car, @Context Locale locale, @Context String userId);

    // ObjectFactory receives @Context for custom instantiation
    @ObjectFactory
    public CarDto createCarDto(@Context Locale locale, @Context String userId) {
        CarDto dto = new CarDto();
        dto.setContextInfo("Created for user: " + userId + " in locale: " + locale.getDisplayName());
        System.out.println("ObjectFactory: Initialized DTO with context - " + userId);
        return dto;
    }

    // Lifecycle methods receive @Context automatically
    @BeforeMapping
    void beforeMapping(Car car, @MappingTarget CarDto dto, @Context Locale locale, @Context String userId) {
        System.out.println("BeforeMapping: Pre-processing with " + locale.getDisplayName() + " for user " + userId);
    }

    @AfterMapping
    void afterMapping(Car car, @MappingTarget CarDto dto, @Context Locale locale, @Context String userId) {
        System.out.println("AfterMapping: Finalizing with " + locale.getDisplayName() + " for user " + userId);
        dto.setFullModel(dto.getMake() + " " + car.getModel() + " (" + locale.getCountry() + ")");
    }
}

// Custom qualifier helper (also receives @Context)
class LocalDateUtils {
    @Named("formatDate")
    public static String format(LocalDate date, @Context Locale locale) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM yyyy", locale);
        return date.format(formatter);
    }
}

// Demo runner
class ContextDemo {
    public static void main(String[] args) {
        CarMapper mapper = Mappers.getMapper(CarMapper.class);

        Car car = new Car("Toyota", "Camry", LocalDate.of(2020, 5, 15));

        // Provide @Context explicitly (Locale + custom userId)
        CarDto dto = mapper.toDto(car, Locale.forLanguageTag("de-DE"), "john.doe@company.com");

        System.out.println("\n=== Final Result ===");
        System.out.println("Make: " + dto.getMake());
        System.out.println("Full Model: " + dto.getFullModel());
        System.out.println("Build Date: " + dto.getFormattedBuildDate());
        System.out.println("Context Info: " + dto.getContextInfo());
    }
}

Expected output showing full lifecycle flow:

ObjectFactory: Initialized DTO with context - john.doe@company.com
BeforeMapping: Pre-processing with German (Germany) for user john.doe@company.com
AfterMapping: Finalizing with German (Germany) for user john.doe@company.com

=== Final Result ===
Make: Toyota
Full Model: Toyota Camry (DE)
Build Date: Mai 2020
Context Info: Created for user: john.doe@company.com in locale: German (Germany)

Lifecycle Integration Details

The complete flow works as: ObjectFactory@BeforeMappingproperty mappings@AfterMapping. Each receives @Context parameters automatically when declared with matching signatures. @ObjectFactory handles target instantiation (useful for dependency injection), while lifecycle methods enable pre/post-processing with full context awareness.

This pattern scales perfectly for enterprise applications requiring audit trails, localization, or service lookups during mapping.

MapStruct Mapper Composition: Pure Java

Mapper composition in MapStruct creates modular mappings by delegating between mappers using @Mapper(uses = {...}). The examples below are fully self-contained, compilable classes with complete domain models, mappers, and main methods.

Complete Domain Classes

import java.time.LocalDate;

// Source domain classes
public class Address {
    private String street;
    private String city;

    public Address(String street, String city) {
        this.street = street;
        this.city = city;
    }

    // Getters and setters
    public String getStreet() { return street; }
    public void setStreet(String street) { this.street = street; }
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }
}

public class User {
    private String name;
    private Address address;

    public User(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    // Getters and setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Address getAddress() { return address; }
    public void setAddress(Address address) { this.address = address; }
}

// Extended User for annotation example
public class UserEnriched {
    private String firstName;
    private String lastName;
    private int age;
    private Address address;

    public UserEnriched(String firstName, String lastName, int age, Address address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.address = address;
    }

    // Getters
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public Address getAddress() { return address; }
}

// Target DTO classes
public class AddressDto {
    private String street;
    private String city;

    public AddressDto() {}

    // Getters and setters
    public String getStreet() { return street; }
    public void setStreet(String street) { this.street = street; }
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }

    @Override
    public String toString() {
        return street + ", " + city;
    }
}

public class UserDto {
    private String name;
    private AddressDto address;

    public UserDto() {}

    // Getters and setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public AddressDto getAddress() { return address; }
    public void setAddress(AddressDto address) { this.address = address; }

    @Override
    public String toString() {
        return name + " <" + address + ">";
    }
}

public class UserEnrichedDto {
    private String fullName;
    private String ageGroup;
    private AddressDto address;

    public UserEnrichedDto() {}

    // Getters and setters
    public String getFullName() { return fullName; }
    public void setFullName(String fullName) { this.fullName = fullName; }
    public String getAgeGroup() { return ageGroup; }
    public void setAgeGroup(String ageGroup) { this.ageGroup = ageGroup; }
    public AddressDto getAddress() { return address; }
    public void setAddress(AddressDto address) { this.address = address; }

    @Override
    public String toString() {
        return fullName + " (" + ageGroup + ") <" + address + ">";
    }
}

Basic Composition Example

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface AddressMapper {
    AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

    AddressDto toDto(Address address);
}

@Mapper(uses = AddressMapper.class)
public interface UserMapper {
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

    UserDto toDto(User user);
}

// Runnable test
public class BasicComposition {
    public static void main(String[] args) {
        User user = new User("John Doe",
            new Address("123 Main St", "Auckland"));

        UserDto dto = UserMapper.INSTANCE.toDto(user);

        System.out.println(dto);
        // Output: John Doe <123 Main St, Auckland>
    }
}

Multiple Mapper and Custom Conversion Composition

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import java.time.format.DateTimeFormatter;
import java.time.LocalDate;

public class DateUtils {
    public static String toString(LocalDate date) {
        if (date == null) {
            return null;
        }
        return date.format(DateTimeFormatter.ISO_LOCAL_DATE);
    }
}

public class Event {
    private String title;
    private LocalDate date;
    private Address address;

    public Event(String title, LocalDate date, Address address) {
        this.title = title;
        this.date = date;
        this.address = address;
    }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public LocalDate getDate() { return date; }
    public void setDate(LocalDate date) { this.date = date; }
    public Address getAddress() { return address; }
    public void setAddress(Address address) { this.address = address; }
}

public class EventDto {
    private String title;
    private String dateString;
    private AddressDto address;

    public EventDto() {}

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getDateString() { return dateString; }
    public void setDateString(String dateString) { this.dateString = dateString; }
    public AddressDto getAddress() { return address; }
    public void setAddress(AddressDto address) { this.address = address; }

    @Override
    public String toString() {
        return title + " on " + dateString + " at " + address;
    }
}

@Mapper(uses = {AddressMapper.class, DateUtils.class})
public interface EventMapper {
    EventMapper INSTANCE = Mappers.getMapper(EventMapper.class);

    @Mapping(source = "date", target = "dateString")
    EventDto toDto(Event event);
}

// Runnable test
public class CustomComposition {
    public static void main(String[] args) {
        Event event = new Event("Tech Conference",
            LocalDate.of(2026, 3, 15),
            new Address("Convention Centre", "Auckland"));

        EventDto dto = EventMapper.INSTANCE.toDto(event);

        System.out.println(dto);
        // Output: Tech Conference on 2026-03-15 at Convention Centre, Auckland
    }
}

Advanced: Reusable Annotation Composition

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.Qualifier;
import org.mapstruct.factory.Mappers;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// Custom utility for age grouping
public class AgeUtils {
    @Named("ageToGroup")
    public static String ageToGroup(int age) {
        if (age < 18) return "Minor";
        if (age < 65) return "Adult";
        return "Senior";
    }
}

// 1. Custom composed annotation (bundles multiple @Mapping rules)
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
@Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())")
@Mapping(target = "ageGroup", source = "age", qualifiedByName = "ageToGroup")
public @interface UserEnrichment {}

// 2. Mapper using the composed annotation
@Mapper(uses = {AddressMapper.class, AgeUtils.class})
public interface UserEnrichedMapper {
    UserEnrichedMapper INSTANCE = Mappers.getMapper(UserEnrichedMapper.class);

    @UserEnrichment  // <- Single annotation replaces 3+ @Mapping entries!
    UserEnrichedDto toEnrichedDto(UserEnriched user);
}

// Runnable test
public class AnnotationComposition {
    public static void main(String[] args) {
        UserEnriched user = new UserEnriched(
            "John", "Doe", 42,
            new Address("123 Main St", "Auckland")
        );

        UserEnrichedDto dto = UserEnrichedMapper.INSTANCE.toEnrichedDto(user);

        System.out.println(dto);
        // Output: John Doe (Adult) <123 Main St, Auckland>
    }
}

Global Configuration

See the Shared MapperConfig in MapStruct: Pure Java

Key Benefits Summary

Feature Pure Java Benefit
uses = {...} Automatic instantiation, no DI container
Mappers.getMapper() Single point of access per mapper
@MapperConfig Consistent behavior without framework
Multiple uses Type-based delegation selection
@UserEnrichment Zero-boilerplate reusable mapping rules
Custom utils Seamless integration with plain classes

Each example compiles and runs independently after MapStruct annotation processing, demonstrating self-contained mapper hierarchies without external dependencies.

Shared MapperConfig in MapStruct: Pure Java

MapStruct's @MapperConfig centralizes common mapping rules, global settings, and shared utilities across multiple mappers. Without Spring, mappers generate plain POJOs for manual instantiation.

Why Shared @MapperConfig?

  • DRY principle: Define ignore rules, nullValuePropertyMappingStrategy, etc. once
  • Consistency: All mappers share identical base behavior
  • Inheritance: Prototype methods propagate @Mapping annotations to concrete mappers
  • Flexibility: Works as interface (pure config) or abstract class (config + utilities)

Complete Domain Models

public class BaseDto {
    private Long id;
    private Long createdAt;
    private Long version;

    // constructors, getters, setters
    public BaseDto() {}

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Long createdAt) {
        this.createdAt = createdAt;
    }

    public Long getVersion() {
        return version;
    }

    public void setVersion(Long version) {
        this.version = version;
    }
}

public class BaseEntity {
    private Long id;
    private Long createdAt;
    private Long version;
    private Long lastModified;

    // constructors, getters, setters
    public BaseEntity() {}

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Long createdAt) {
        this.createdAt = createdAt;
    }

    public Long getVersion() {
        return version;
    }

    public void setVersion(Long version) {
        this.version = version;
    }

    public Long getLastModified() {
        return lastModified;
    }

    public void setLastModified(Long lastModified) {
        this.lastModified = lastModified;
    }
}

public class CustomerDto extends BaseDto {
    private String firstName;
    private String lastName;
    private String email;

    // constructors, getters, setters
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

public class Customer extends BaseEntity {
    private String firstName;
    private String lastName;
    private String customerEmail;

    // constructors, getters, setters
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getCustomerEmail() {
        return customerEmail;
    }

    public void setCustomerEmail(String customerEmail) {
        this.customerEmail = customerEmail;
    }
}

public class OrderDto extends BaseDto {
    private String orderNumber;
    private CustomerDto customer;

    // constructors, getters, setters
    public String getOrderNumber() {
        return orderNumber;
    }

    public void setOrderNumber(String orderNumber) {
        this.orderNumber = orderNumber;
    }

    public CustomerDto getCustomer() {
        return customer;
    }

    public void setCustomer(CustomerDto customer) {
        this.customer = customer;
    }
}

public class Order extends BaseEntity {
    private String orderNumber;
    private String customerEmail;
    private String status;

    // constructors, getters, setters
    public String getOrderNumber() {
        return orderNumber;
    }

    public void setOrderNumber(String orderNumber) {
        this.orderNumber = orderNumber;
    }

    public String getCustomerEmail() {
        return customerEmail;
    }

    public void setCustomerEmail(String customerEmail) {
        this.customerEmail = customerEmail;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
}

@MapperConfig as Interface (Pure Configuration)

GlobalMapperConfig.java:

import org.mapstruct.MapperConfig;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
import org.mapstruct.MappingInheritanceStrategy;

@MapperConfig(
    unmappedTargetPolicy = ReportingPolicy.ERROR,
    mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_FROM_CONFIG
)
public interface GlobalMapperConfig {

    @Mapping(target = "id", ignore = true)
    @Mapping(target = "createdAt", ignore = true)
    @Mapping(target = "version", ignore = true)
    BaseEntity toBaseEntity(BaseDto dto);
}

CustomerMapper.java:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;

@Mapper(config = GlobalMapperConfig.class)
public interface CustomerMapper {

    // AUTO inherits ignore rules for id/createdAt/version
    @Mapping(target = "customerEmail", source = "email")
    @Mapping(target = "lastModified", ignore = true)
    Customer toEntity(CustomerDto dto);

    @Mapping(target = "customerEmail", source = "email")
    @Mapping(target = "lastModified", ignore = true)
    void updateEntity(CustomerDto dto, @MappingTarget Customer entity);
}

Generated CustomerMapperImpl.java:

import javax.annotation.processing.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-9.2.0.jar, environment: Java 25 (Oracle Corporation)"
)
public class CustomerMapperImpl implements CustomerMapper {

    @Override
    public Customer toEntity(CustomerDto dto) {
        if ( dto == null ) {
            return null;
        }

        Customer customer = new Customer();

        customer.setCustomerEmail( dto.getEmail() );
        customer.setFirstName( dto.getFirstName() );
        customer.setLastName( dto.getLastName() );

        return customer;
    }

    @Override
    public void updateEntity(CustomerDto dto, Customer entity) {
        if ( dto == null ) {
            return;
        }

        entity.setCustomerEmail( dto.getEmail() );
        entity.setFirstName( dto.getFirstName() );
        entity.setLastName( dto.getLastName() );
    }
}

@MapperConfig in Abstract Mapper Class

BaseEntityMapper.java:

import org.mapstruct.BeforeMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.Named;

@Mapper(config = GlobalMapperConfig.class)
public abstract class BaseEntityMapper {

    // Inherits GlobalMapperConfig ignore rules
    @Mapping(target = "lastModified", ignore = true)
    public abstract BaseEntity toEntity(BaseDto dto);

    // Shared utility method
    @Named("normalizeEmail")
    protected String normalizeEmail(String email) {
        return email != null ? email.trim().toLowerCase() : null;
    }

    @Mapping(target = "lastModified", ignore = true)
    public abstract void updateEntity(BaseDto dto, @MappingTarget BaseEntity entity);

    @BeforeMapping
    protected void enrichTimestamps(BaseDto dto, @MappingTarget BaseEntity entity) {
        entity.setLastModified(System.currentTimeMillis());
    }
}

Generated BaseEntityMapperImpl.java:

import javax.annotation.processing.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-9.2.0.jar, environment: Java 25 (Oracle Corporation)"
)
public class BaseEntityMapperImpl extends BaseEntityMapper {

    @Override
    public BaseEntity toEntity(BaseDto dto) {
        if ( dto == null ) {
            return null;
        }

        BaseEntity baseEntity = new BaseEntity();

        enrichTimestamps( dto, baseEntity );

        return baseEntity;
    }

    @Override
    public void updateEntity(BaseDto dto, BaseEntity entity) {
        if ( dto == null ) {
            return;
        }

        enrichTimestamps( dto, entity );
    }
}

OrderMapper.java:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(config = GlobalMapperConfig.class, uses = BaseEntityMapper.class)
public abstract class OrderMapper {

    // Inherits GlobalConfig + BaseEntityMapper utilities
    @Mapping(target = "customerEmail", source = "customer.email", qualifiedByName = "normalizeEmail")
    @Mapping(target = "status", constant = "PENDING")
    @Mapping(target = "lastModified", ignore = true)
    public abstract Order toEntity(OrderDto dto);

    public Order createProcessedOrder(OrderDto dto) {
        Order order = toEntity(dto);
        order.setStatus("PROCESSED");
        return order;
    }
}

Generated OrderMapperImpl.java:

import javax.annotation.processing.Generated;
import org.mapstruct.factory.Mappers;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-9.2.0.jar, environment: Java 25 (Oracle Corporation)"
)
public class OrderMapperImpl extends OrderMapper {

    private final BaseEntityMapper baseEntityMapper = Mappers.getMapper( BaseEntityMapper.class );

    @Override
    public Order toEntity(OrderDto dto) {
        if ( dto == null ) {
            return null;
        }

        Order order = new Order();

        baseEntityMapper.enrichTimestamps( dto, order );

        order.setCustomerEmail( baseEntityMapper.normalizeEmail( dtoCustomerEmail( dto ) ) );
        order.setOrderNumber( dto.getOrderNumber() );

        order.setStatus( "PENDING" );

        return order;
    }

    private String dtoCustomerEmail(OrderDto orderDto) {
        CustomerDto customer = orderDto.getCustomer();
        if ( customer == null ) {
            return null;
        }
        return customer.getEmail();
    }
}

Usage in Application Code

public class OrderService {
    private final CustomerMapper customerMapper = new CustomerMapperImpl();
    private final OrderMapper orderMapper = new OrderMapperImpl();

    public Customer createCustomer(CustomerDto dto) {
        return customerMapper.toEntity(dto);
    }

    public Order processOrder(OrderDto dto) {
        // Gets 3 layers of configuration
        return orderMapper.createProcessedOrder(dto);
    }
}

// Fluent factory (alternative)
// import org.mapstruct.factory.Mappers;
// CustomerMapper mapper = Mappers.getMapper(CustomerMapper.class);
// Customer customer = mapper.toEntity(dto);

Inheritance Layers Summary

Layer Source Provides
GlobalMapperConfig Interface id/createdAt/version ignore rules
BaseEntityMapper Abstract class + normalizeEmail(), @BeforeMapping
CustomerMapper Interface Specific Customer mappings
OrderMapper Abstract class Specific Order mappings + createProcessedOrder()

Key Benefits

  1. Scalable hierarchy - Global → Base → Specific mappers
  2. Full type safety - No runtime surprises

This pattern handles dozens of entities while maintaining clean, consistent mapping behavior across your entire application.

Java Stream Reduce: A Practical Guide

Java Stream Reduce: A Practical Guide

Java Streams' reduce operation transforms a sequence of elements into a single result through repeated application of an accumulator function, embodying the essence of functional reduction patterns.

Core Method Overloads

Three primary signatures handle different scenarios. The basic Optional<T> reduce(BinaryOperator<T> accumulator) pairwise combines elements, returning Optional for empty stream safety.

The identity form T reduce(T identity, BinaryOperator<T> accumulator) supplies a starting value like 0 for sums, guaranteeing results even from empty streams.​

Advanced reduce(T identity, BiFunction<T,? super U,R> accumulator, BinaryOperator<R> combiner) supports parallel execution and type conversion from stream <T> to result <R>.

Reduction folds elements left-to-right: begin with identity (or first element), accumulate each subsequent item. For [1,2,3] summing, compute ((0+1)+2)+3.

Parallel streams divide work into subgroups, requiring an associative combiner to merge partial results reliably.

Basic Reductions

Sum integers: int total = IntStream.range(1, 11).reduce(0, Integer::sum); // 55.

Maximum value: OptionalInt max = IntStream.range(1, 11).reduce(Math::max); //OptionalInt[10].

String concatenation: String joined = Stream.of("Hello", " ", "World").reduce("", String::concat); //Hello World.

Object comparison:

record Car(String model, int price) {
}

var cars = List.of(
    new Car("Model A", 20000),
    new Car("Model B", 30000),
    new Car("Model C", 25000)
);

Optional<Car> priciest = cars.stream().reduce((c1,c2) -> c1.price > c2.price ? c1 : c2); //Optional[Car[model=Model B, price=30000]]

Advanced: Different Types

Three-argument overload converts stream <T> to result <R>:

// IntStream → formatted String
String squares = IntStream.of(1,2,3)
    .boxed()
    .reduce("",
        (accStr, num) -> accStr + (num * num) + ", ",
        String::concat);  // "1, 4, 9, "

Employee list → summary:

record Employee(String name, String dept) {
}

var employees = List.of(
    new Employee("John", "IT"),
    new Employee("Tom", "Sales")
);

String summary = employees.stream()
        .reduce("",
                (acc, emp) -> acc + emp.name() + "-" + emp.dept() + " | ",
                String::concat);  // "John-IT | Tom-Sales | "

Parallel execution demands the combiner to merge thread-local String partials.

Sequential Execution Parallel (with combiner)
((0+1)+2)+3 (0+1) + (2+3)1+5
""+"1"+"4" ""+"1" + ""+"4""1"+"4"

Performance Tips

Use parallelStream() with proper combiner: list.parallelStream().reduce(0, (a,b)->a+b, Integer::sum).

Opt for primitive streams (IntStream, LongStream) to eliminate boxing overhead.

Prefer sum(), max(), collect(joining()) for simple cases; reserve custom reduce for complex logic or type transformations.

Data-Oriented Programming in Modern Java

Data-oriented programming (DOP) in Java emphasizes immutable data structures separated from business logic, leveraging modern features like records, sealed interfaces, and pattern matching for safer, more maintainable code.

Core Principles of DOP

DOP models data transparently using plain structures that fully represent domain concepts without hidden behavior or mutable state. Key rules include making data immutable, explicitly modeling all variants with sealed types, preventing illegal states at the type level, and handling validation at boundaries.

This contrasts with traditional OOP by keeping data passive and logic in pure functions, improving testability and reducing coupling.

Java's Support for DOP

Records provide concise, immutable data carriers with built-in equality and toString. Sealed interfaces define closed hierarchies for exhaustive handling, while pattern matching in switch and instanceof enables declarative operations on variants.

These features combine to enforce exhaustiveness at compile time, eliminating visitor patterns or runtime checks.

Practical Example: Geometric Shapes

Model 2D shapes to compute centers, showcasing DOP in action.

sealed interface Shape permits Circle, Rectangle, Triangle {}

record Point(double x, double y) {}

record Circle(Point center, double radius) implements Shape {}

record Rectangle(Point topLeft, Point bottomRight) implements Shape {}

record Triangle(Point p1, Point p2, Point p3) implements Shape {}

Operations remain separate and pure:

public static Point centerOf(Shape shape) {
    return switch (shape) {
        case Circle c -> c.center();
        case Rectangle r -> new Point(
            (r.topLeft().x() + r.bottomRight().x()) / 2.0,
            (r.topLeft().y() + r.bottomRight().y()) / 2.0
        );
        case Triangle t -> new Point(
            (t.p1().x() + t.p2().x() + t.p3().x()) / 3.0,
            (t.p1().y() + t.p2().y() + t.p3().y()) / 3.0
        );
    };
}

The sealed interface ensures exhaustive coverage, records keep data transparent, and the function is stateless.

DOP vs. Traditional OOP

Aspect DOP in Java Traditional OOP
Data Immutable records, sealed variants Mutable objects with fields/methods
Behavior Separate pure functions Embedded in classes
State Handling None; inputs → outputs Mutable instance state
Safety Compile-time exhaustiveness Runtime polymorphism/overrides
Testing Easy unit tests on functions Mocking object interactions

DOP shines in APIs, events, and rules engines by prioritizing data flow over object lifecycles.

« Older posts Newer posts »